Final Project
Building the Console Application
আপনি একটি 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 করে
Mainconcrete 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