Final Project

Building the Console Application

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

আমাদের Final Project-এর এখন তিনটি major layer ready:

Domain
Application Services
File Repositories

এবার আমরা presentation layer তৈরি করব।

এই project-এ presentation layer হবে:

Console application

User menu থেকে operation choose করবে, input দেবে, এবং application service call হবে।

Console layer-এর কাজ:

  • Input read করা
  • Number parse করা
  • Domain value objects তৈরি করা
  • Services call করা
  • Success/error message দেখানো

Console layer-এর কাজ নয়:

Course publication rules
Enrollment transition rules
Duplicate detection
File persistence

এই lesson শেষে application interactiveভাবে run করা যাবে।


Learning Objectives

এই lesson শেষে আপনি পারবেন:

  • Scanner দিয়ে console input read করতে
  • Menu loop design করতে
  • Input parsing আলাদা methods-এ রাখতে
  • Strong domain types তৈরি করতে
  • Services call করতে
  • Expected exceptions presentation boundary-তে handle করতে
  • Domain logic console code-এর বাইরে রাখতে
  • Composition root এবং console lifecycle organize করতে

Target Menu

Application start হলে:

=== LiveKlass Course Enrollment System ===

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
0. Exit

User একটি option select করবে।


Presentation Boundary

Conceptually:

User
 ↓
Console
 ↓
Application Services
 ↓
Domain + Repositories

Console জানে:

কী input নিতে হবে
কোন service method call করতে হবে
কী output দেখাতে হবে

কিন্তু console জানে না:

Course কীভাবে file-এ save হয়
Course publish করার exact rules কী
Enrollment duplicate কিনা কীভাবে detect হয়

Avoid This Design

Bad:

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

inside console code।

Better:

courseService.publishCourse(
        courseCode
);

Business rules service/domain layer-এ থাকে।


Console Class

আমরা একটি dedicated class ব্যবহার করব:

ConsoleApplication

It receives:

CourseService
LearnerService
EnrollmentService

through constructor injection।


ConsoleApplication.java

package io.liveklass.console;

import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseService;
import io.liveklass.course.CourseSummary;
import io.liveklass.course.Lesson;
import io.liveklass.course.LessonId;
import io.liveklass.enrollment.EnrollmentId;
import io.liveklass.enrollment.EnrollmentService;
import io.liveklass.enrollment.EnrollmentSummary;
import io.liveklass.learner.EmailAddress;
import io.liveklass.learner.Learner;
import io.liveklass.learner.LearnerId;
import io.liveklass.learner.LearnerService;

import java.util.Scanner;

public final class ConsoleApplication {

    private final CourseService courseService;
    private final LearnerService learnerService;
    private final EnrollmentService enrollmentService;
    private final Scanner scanner;

    public ConsoleApplication(
            CourseService courseService,
            LearnerService learnerService,
            EnrollmentService enrollmentService,
            Scanner scanner
    ) {
        if (
                courseService == null
                || learnerService == null
                || enrollmentService == null
                || scanner == null
        ) {
            throw new IllegalArgumentException(
                    "Console dependencies are required."
            );
        }

        this.courseService =
                courseService;

        this.learnerService =
                learnerService;

        this.enrollmentService =
                enrollmentService;

        this.scanner =
                scanner;
    }

    public void run() {
        boolean running =
                true;

        while (running) {
            printMenu();

            String choice =
                    readLine(
                            "Choose option: "
                    );

            try {
                switch (choice) {
                    case "1" ->
                            createCourse();

                    case "2" ->
                            addLesson();

                    case "3" ->
                            publishCourse();

                    case "4" ->
                            archiveCourse();

                    case "5" ->
                            listCourses();

                    case "6" ->
                            registerLearner();

                    case "7" ->
                            listLearners();

                    case "8" ->
                            enrollLearner();

                    case "9" ->
                            completeEnrollment();

                    case "10" ->
                            cancelEnrollment();

                    case "11" ->
                            listEnrollments();

                    case "0" ->
                            running =
                                    false;

                    default ->
                            System.out.println(
                                    "Unknown option."
                            );
                }
            } catch (
                RuntimeException exception
            ) {
                System.out.println(
                        "Error: "
                        + exception.getMessage()
                );
            }

            System.out.println();
        }

        System.out.println(
                "Goodbye."
        );
    }

    private void printMenu() {
        System.out.println(
                "=== LiveKlass Course Enrollment System ==="
        );

        System.out.println(
                "1. Create course"
        );

        System.out.println(
                "2. Add lesson"
        );

        System.out.println(
                "3. Publish course"
        );

        System.out.println(
                "4. Archive course"
        );

        System.out.println(
                "5. List courses"
        );

        System.out.println(
                "6. Register learner"
        );

        System.out.println(
                "7. List learners"
        );

        System.out.println(
                "8. Enroll learner"
        );

        System.out.println(
                "9. Complete enrollment"
        );

        System.out.println(
                "10. Cancel enrollment"
        );

        System.out.println(
                "11. List enrollments"
        );

        System.out.println(
                "0. Exit"
        );
    }

    private void createCourse() {
        CourseCode code =
                new CourseCode(
                        readLine(
                                "Course code: "
                        )
                );

        String title =
                readLine(
                        "Title: "
                );

        long price =
                readLong(
                        "Price in paisa: "
                );

        courseService.createCourse(
                code,
                title,
                price
        );

        System.out.println(
                "Course created successfully."
        );
    }

    private void addLesson() {
        CourseCode courseCode =
                new CourseCode(
                        readLine(
                                "Course code: "
                        )
                );

        LessonId lessonId =
                new LessonId(
                        readLong(
                                "Lesson id: "
                        )
                );

        String title =
                readLine(
                        "Lesson title: "
                );

        String content =
                readLine(
                        "Lesson content: "
                );

        Lesson lesson =
                new Lesson(
                        lessonId,
                        title,
                        content
                );

        courseService.addLesson(
                courseCode,
                lesson
        );

        System.out.println(
                "Lesson added successfully."
        );
    }

    private void publishCourse() {
        CourseCode courseCode =
                readCourseCode();

        courseService.publishCourse(
                courseCode
        );

        System.out.println(
                "Course published successfully."
        );
    }

    private void archiveCourse() {
        CourseCode courseCode =
                readCourseCode();

        courseService.archiveCourse(
                courseCode
        );

        System.out.println(
                "Course archived successfully."
        );
    }

    private void listCourses() {
        var courses =
                courseService.findAllCourses();

        if (courses.isEmpty()) {
            System.out.println(
                    "No courses found."
            );

            return;
        }

        for (
                CourseSummary course
                : courses
        ) {
            System.out.printf(
                    "%s | %s | %d | %s | lessons=%d%n",
                    course.code(),
                    course.title(),
                    course.priceInPaisa(),
                    course.status(),
                    course.lessonCount()
            );
        }
    }

    private void registerLearner() {
        LearnerId learnerId =
                new LearnerId(
                        readLong(
                                "Learner id: "
                        )
                );

        String name =
                readLine(
                        "Name: "
                );

        EmailAddress email =
                new EmailAddress(
                        readLine(
                                "Email: "
                        )
                );

        learnerService.registerLearner(
                learnerId,
                name,
                email
        );

        System.out.println(
                "Learner registered successfully."
        );
    }

    private void listLearners() {
        var learners =
                learnerService.findAllLearners();

        if (learners.isEmpty()) {
            System.out.println(
                    "No learners found."
            );

            return;
        }

        for (
                Learner learner
                : learners
        ) {
            System.out.printf(
                    "%s | %s | %s%n",
                    learner.id(),
                    learner.name(),
                    learner.email()
            );
        }
    }

    private void enrollLearner() {
        EnrollmentId enrollmentId =
                new EnrollmentId(
                        readLong(
                                "Enrollment id: "
                        )
                );

        LearnerId learnerId =
                new LearnerId(
                        readLong(
                                "Learner id: "
                        )
                );

        CourseCode courseCode =
                readCourseCode();

        enrollmentService.enroll(
                enrollmentId,
                learnerId,
                courseCode
        );

        System.out.println(
                "Enrollment created successfully."
        );
    }

    private void completeEnrollment() {
        EnrollmentId enrollmentId =
                readEnrollmentId();

        enrollmentService.completeEnrollment(
                enrollmentId
        );

        System.out.println(
                "Enrollment completed successfully."
        );
    }

    private void cancelEnrollment() {
        EnrollmentId enrollmentId =
                readEnrollmentId();

        enrollmentService.cancelEnrollment(
                enrollmentId
        );

        System.out.println(
                "Enrollment cancelled successfully."
        );
    }

    private void listEnrollments() {
        var enrollments =
                enrollmentService
                        .findAllEnrollments();

        if (enrollments.isEmpty()) {
            System.out.println(
                    "No enrollments found."
            );

            return;
        }

        for (
                EnrollmentSummary enrollment
                : enrollments
        ) {
            System.out.printf(
                    "%s | learner=%s | course=%s | %s%n",
                    enrollment.id(),
                    enrollment.learnerId(),
                    enrollment.courseCode(),
                    enrollment.status()
            );
        }
    }

    private CourseCode readCourseCode() {
        return new CourseCode(
                readLine(
                        "Course code: "
                )
        );
    }

    private EnrollmentId readEnrollmentId() {
        return new EnrollmentId(
                readLong(
                        "Enrollment id: "
                )
        );
    }

    private String readLine(
            String prompt
    ) {
        System.out.print(
                prompt
        );

        return scanner.nextLine();
    }

    private long readLong(
            String prompt
    ) {
        String value =
                readLine(
                        prompt
                );

        try {
            return Long.parseLong(
                    value.strip()
            );
        } catch (
            NumberFormatException exception
        ) {
            throw new IllegalArgumentException(
                    "Expected a valid number."
            );
        }
    }
}

Why Use nextLine() Everywhere?

Scanner provides:

nextLong()
nextInt()
nextLine()

Mixing them often creates confusing newline behavior।

Example:

scanner.nextLong();
scanner.nextLine();

The remaining newline may be consumed unexpectedly।

A simpler strategy:

Always read String using nextLine()
Then parse manually

That is why:

readLong(...)

first calls:

readLine(...)

and then:

Long.parseLong(...)

Input Parsing Boundary

Console receives raw:

"java-oop"
"1001"
"sakib@example.com"

Then converts these to:

CourseCode
EnrollmentId
EmailAddress

Once converted successfully, downstream code receives strong domain types।


Invalid Number Example

Input:

Enrollment id: abc

Long.parseLong() throws:

NumberFormatException

Console converts it into:

IllegalArgumentException(
        "Expected a valid number."
)

Then the main loop displays:

Error: Expected a valid number.

Invalid Domain Input Example

Input:

Course code:

blank।

This:

new CourseCode(
        ""
)

throws:

IllegalArgumentException

Console displays:

Error: Course code is required.

No invalid CourseCode reaches the service।


Error Boundary

Our menu loop contains:

try {
    ...
} catch (
    RuntimeException exception
) {
    System.out.println(
            "Error: "
            + exception.getMessage()
    );
}

This means expected application failures do not terminate the whole program।

Example:

Course not found
Duplicate enrollment
Invalid state
Invalid number
Storage failure

are presented at the console boundary।


Should We Catch RuntimeException Everywhere?

No।

This is acceptable here because:

Console loop is the top-level application boundary.

Inside domain/service code, do not repeatedly catch and hide exceptions।

In a larger application you would usually distinguish:

Expected application errors
Unexpected programming errors
Infrastructure failures

more carefully।


Never Do This

Bad:

try {
    service.publishCourse(
            code
    );
} catch (
    Exception ignored
) {
}

This silently loses failure information।

At minimum, application boundary should surface the error।


Console Should Not Know Repository Types

ConsoleApplication receives:

CourseService
LearnerService
EnrollmentService

It does not import:

FileCourseRepository
FileLearnerRepository
FileEnrollmentRepository

Presentation should not care how data is stored।


Complete Main

Now we wire the application।

Main.java

package io.liveklass;

import io.liveklass.console.ConsoleApplication;
import io.liveklass.course.CourseRepository;
import io.liveklass.course.CourseService;
import io.liveklass.enrollment.EnrollmentRepository;
import io.liveklass.enrollment.EnrollmentService;
import io.liveklass.learner.LearnerRepository;
import io.liveklass.learner.LearnerService;
import io.liveklass.storage.FileCourseRepository;
import io.liveklass.storage.FileEnrollmentRepository;
import io.liveklass.storage.FileLearnerRepository;

import java.nio.file.Path;
import java.util.Scanner;

public class Main {

    public static void main(
            String[] args
    ) {
        Path dataDirectory =
                Path.of(
                        "data"
                );

        CourseRepository courseRepository =
                new FileCourseRepository(
                        dataDirectory.resolve(
                                "courses"
                        )
                );

        LearnerRepository learnerRepository =
                new FileLearnerRepository(
                        dataDirectory.resolve(
                                "learners"
                        )
                );

        EnrollmentRepository enrollmentRepository =
                new FileEnrollmentRepository(
                        dataDirectory.resolve(
                                "enrollments"
                        )
                );

        CourseService courseService =
                new CourseService(
                        courseRepository
                );

        LearnerService learnerService =
                new LearnerService(
                        learnerRepository
                );

        EnrollmentService enrollmentService =
                new EnrollmentService(
                        courseRepository,
                        learnerRepository,
                        enrollmentRepository
                );

        try (
            Scanner scanner =
                    new Scanner(
                            System.in
                    )
        ) {
            ConsoleApplication application =
                    new ConsoleApplication(
                            courseService,
                            learnerService,
                            enrollmentService,
                            scanner
                    );

            application.run();
        }
    }
}

Main Is the Composition Root

Main decides:

Which repository implementations?
Which service instances?
Which scanner?
How are they connected?

Domain objects do not know this।

Services do not know concrete storage classes।

Console does not know repositories।

This is a clean object graph।


Complete Dependency Flow

Main
│
├── FileCourseRepository
├── FileLearnerRepository
├── FileEnrollmentRepository
│
├── CourseService
├── LearnerService
├── EnrollmentService
│
└── ConsoleApplication

Dependencies are explicit।


Example Session

=== LiveKlass Course Enrollment System ===
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
0. Exit

Choose option: 1
Course code: java-oop
Title: Java and OOP Foundation
Price in paisa: 499000

Course created successfully.

Then:

Choose option: 2
Course code: java-oop
Lesson id: 1
Lesson title: Introduction to Java
Lesson content: Java fundamentals

Lesson added successfully.

Then:

Choose option: 3
Course code: java-oop

Course published successfully.

Register Learner

Choose option: 6
Learner id: 1
Name: Sakib
Email: sakib@example.com

Learner registered successfully.

Enrollment

Choose option: 8
Enrollment id: 1001
Learner id: 1
Course code: java-oop

Enrollment created successfully.

Invalid Duplicate Enrollment

Second attempt:

Error: Learner 1 is already enrolled in course JAVA-OOP.

Console did not calculate duplicate enrollment itself।

It simply presented the service failure।


List Output

Course list:

JAVA-OOP | Java and OOP Foundation | 499000 | PUBLISHED | lessons=1

Learner list:

1 | Sakib | sakib@example.com

Enrollment list:

1001 | learner=1 | course=JAVA-OOP | ACTIVE

Presentation Formatting

We currently print:

499000

for price in paisa।

Could console convert it into:

4990.00

?

Yes।

Presentation formatting is a console responsibility।

But domain should still store:

long priceInPaisa

as an exact integer minor-unit value।


Example Price Formatter

private String formatPrice(
        long priceInPaisa
) {
    return String.format(
            "%.2f",
            priceInPaisa / 100.0
    );
}

For financial production systems, floating-point conversion deserves more care, but simple display formatting is enough for this project।


Avoid Business Decisions in Formatting

Fine:

Convert paisa to display string

Not fine:

If price is 0 automatically publish course

Presentation should format data, not invent domain rules।


Console Method Size

Each operation method is small:

Read inputs
Construct values
Call service
Print result

Example:

private void publishCourse() {
    CourseCode courseCode =
            readCourseCode();

    courseService.publishCourse(
            courseCode
    );

    System.out.println(
            "Course published successfully."
    );
}

This is easy to understand।


Why Not One Giant run()?

Weak:

public void run() {
    // 500 lines of menu,
    // parsing,
    // business logic,
    // file operations
}

Better:

run()
→ dispatch operation
→ focused private method

This keeps the main loop readable।


Input Helper Methods

We created:

readLine(...)
readLong(...)
readCourseCode()
readEnrollmentId()

These remove repeated parsing noise।

But avoid creating dozens of tiny helpers without meaningful reuse।


What About EOF?

If console input closes unexpectedly:

scanner.nextLine()

may throw:

NoSuchElementException

For this learning project, interactive terminal input is assumed।

Production command-line tools may handle end-of-input explicitly।


Console and Storage Failures

Suppose disk permission prevents saving.

Repository throws:

StorageException

It travels through service unchanged to the top-level console boundary।

Console displays its message।

This keeps failure translation layered:

IOException
↓
StorageException
↓
Console message

What Should Not Be Printed to Users?

A production application should generally not expose:

Full stack traces
Filesystem secrets
Credentials
Sensitive internal state

Our console displays:

exception.getMessage()

Stack traces are not shown as normal user output।


Practice Exercise 1

Why do we read numbers using:

nextLine()

then:

Long.parseLong()

instead of mixing nextLong() and nextLine()?

Answer

It avoids common scanner newline-consumption problems and gives one consistent input strategy।


Practice Exercise 2

Where should this check live?

if (
        course has no lessons
) {
    reject publication
}

Answer

Inside:

Course.publish()

not console code।


Practice Exercise 3

Where should this code live?

System.out.printf(
        "%s | %s%n",
        course.code(),
        course.title()
);

Answer

Presentation/console layer।


Practice Exercise 4

Should ConsoleApplication create:

new FileCourseRepository(...)

itself?

Answer

No।

Main should assemble dependencies and pass services to the console।


Practice Exercise 5

What happens when user enters:

Learner id: hello

Answer

readLong() throws a meaningful:

IllegalArgumentException

which the console boundary displays without terminating the application loop।


Knowledge Check

Question 1

What is the responsibility of the console layer?

Question 2

Why should console code not implement domain rules?

Question 3

Why convert raw input into strong value objects early?

Question 4

Why use a top-level error boundary?

Question 5

Why does ConsoleApplication depend on services instead of repositories?

Question 6

What is the role of Main?

Question 7

Why is Scanner closed in Main?

Question 8

Why keep menu operation methods focused?

Question 9

Should presentation code know how Course is serialized?

Question 10

What does the complete application dependency flow look like?


Knowledge Check Answers

Answer 1

Read user input, parse it, call application services, and display results or errors।

Answer 2

Domain rules should have one authoritative owner and remain reusable outside a console interface।

Answer 3

So invalid primitive/string input is rejected at the boundary and downstream code receives validated domain values।

Answer 4

To prevent expected application failures from terminating the interactive program and to present them consistently।

Answer 5

The console performs use cases; repository mechanics are an implementation detail behind the services।

Answer 6

Main is the composition root that creates concrete dependencies and connects the application object graph।

Answer 7

The code that owns the Scanner resource should close it when application execution ends।

Answer 8

Focused methods keep the main application flow readable and separate each user action clearly।

Answer 9

No. Serialization belongs to storage repositories।

Answer 10

Conceptually:

Console
→ Services
→ Domain + Repository Contracts
→ File Repository Implementations

Lesson Summary

এই lesson-এ আমরা Final Project-এর interactive console layer তৈরি করেছি।

আমরা শিখেছি:

  • Console is a presentation boundary
  • Raw input console-এ parse করা উচিত
  • Strong domain types boundary-তেই create করা useful
  • Scanner.nextLine() + explicit parsing consistent input handling দেয়
  • Console business rules implement করে না
  • Console repositories-এর storage details জানে না
  • Services use cases expose করে
  • Top-level error handling application loop alive রাখে
  • Operation-specific private methods readability improve করে
  • Main concrete dependencies assemble করে
  • File repositories, services, and console constructor injection-এর মাধ্যমে connected হয়
  • Application now has complete flow:
User input
→ Console
→ Service
→ Domain
→ Repository
→ File

এখন আমাদের Final Project functionally complete।


Next Lesson

পরবর্তী lesson:

Final Project Review and Hardening

আমরা পুরো application review করব:

  • Domain invariants
  • Package boundaries
  • Equality
  • Immutability
  • Defensive copying
  • Exception boundaries
  • File safety
  • Dependency design
  • Common bugs
  • Refactoring opportunities
  • Final assessment checklist