Enums, Exceptions, and Robust Error Handling

Practice and Assessment

ReadingPreview

You are viewing a free preview lesson.

Project Overview

এই final practice project-এ আমরা একটি simplified course-management flow তৈরি করব।

Systemটি support করবে:

  • Course creation request validation
  • Multiple field errors
  • Domain invariants
  • Course status transitions
  • Custom exceptions
  • Repository failure translation
  • Consistent API error responses
  • Correlation IDs
  • Safe external messages

এই project-এ Module 5-এর core concepts একসঙ্গে ব্যবহার করা হবে:

enum
throw
try-catch
checked and unchecked exceptions
custom exceptions
validation results
failure boundaries

Project Requirements

Course Creation

  • Course code required
  • Course title required
  • Price required
  • Price cannot be negative
  • Course code unique হতে হবে

Course Lifecycle

Valid transitions:

DRAFT → REVIEW
REVIEW → DRAFT
REVIEW → PUBLISHED
PUBLISHED → ARCHIVED

Additional rule:

Course must contain at least one lesson before review.

Failure Boundary

Every failure response should contain:

  • Stable error code
  • Safe message
  • Correlation ID
  • Optional field errors

Raw stack traces clientকে return করা যাবে না।


Project Structure

io.liveklass
├── api
├── course
├── exceptions
├── repository
├── service
└── validation

Part 1: Course State

CourseStatus.java

package io.liveklass.course;

public enum CourseStatus {

    DRAFT,
    REVIEW,
    PUBLISHED,
    ARCHIVED;

    public boolean canTransitionTo(
            CourseStatus target
    ) {
        if (target == null) {
            return false;
        }

        return switch (this) {
            case DRAFT ->
                    target == REVIEW;

            case REVIEW ->
                    target == DRAFT
                    || target == PUBLISHED;

            case PUBLISHED ->
                    target == ARCHIVED;

            case ARCHIVED ->
                    false;
        };
    }

    public boolean isEditable() {
        return this == DRAFT
                || this == REVIEW;
    }
}

Part 2: Course Code

CourseCode.java

package io.liveklass.course;

import java.util.Objects;

public final class CourseCode {

    private final String value;

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

        String normalized =
                value.strip()
                        .toUpperCase();

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

        this.value =
                normalized;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                !(other
                instanceof CourseCode courseCode)
        ) {
            return false;
        }

        return value.equals(
                courseCode.value
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }

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

Part 3: Custom Exceptions

CourseNotFoundException.java

package io.liveklass.exceptions;

import io.liveklass.course.CourseCode;

public final class CourseNotFoundException
        extends RuntimeException {

    private final CourseCode courseCode;

    public CourseNotFoundException(
            CourseCode courseCode
    ) {
        super(
                "Course was not found for code "
                + courseCode
                + "."
        );

        this.courseCode =
                courseCode;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

DuplicateCourseCodeException.java

package io.liveklass.exceptions;

import io.liveklass.course.CourseCode;

public final class DuplicateCourseCodeException
        extends RuntimeException {

    private final CourseCode courseCode;

    public DuplicateCourseCodeException(
            CourseCode courseCode
    ) {
        super(
                "Course code is already registered: "
                + courseCode
                + "."
        );

        this.courseCode =
                courseCode;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

InvalidCourseTransitionException.java

package io.liveklass.exceptions;

import io.liveklass.course.CourseStatus;

public final class InvalidCourseTransitionException
        extends RuntimeException {

    private final CourseStatus currentStatus;
    private final CourseStatus targetStatus;

    public InvalidCourseTransitionException(
            CourseStatus currentStatus,
            CourseStatus targetStatus
    ) {
        super(
                "Course cannot transition from "
                + currentStatus
                + " to "
                + targetStatus
                + "."
        );

        this.currentStatus =
                currentStatus;

        this.targetStatus =
                targetStatus;
    }

    public CourseStatus getCurrentStatus() {
        return currentStatus;
    }

    public CourseStatus getTargetStatus() {
        return targetStatus;
    }
}

CourseRepositoryException.java

package io.liveklass.exceptions;

public final class CourseRepositoryException
        extends RuntimeException {

    public CourseRepositoryException(
            String message
    ) {
        super(
                message
        );
    }

    public CourseRepositoryException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Part 4: Course Domain Object

Course.java

package io.liveklass.course;

import io.liveklass.exceptions.InvalidCourseTransitionException;

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

public final class Course {

    private final CourseCode code;
    private final String title;
    private final long priceInPaisa;
    private final List<String> lessons;

    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.lessons =
                new ArrayList<>();

        this.status =
                CourseStatus.DRAFT;
    }

    public void addLesson(
            String lessonTitle
    ) {
        if (!status.isEditable()) {
            throw new IllegalStateException(
                    "Lessons cannot be changed while course status is "
                    + status
                    + "."
            );
        }

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

        lessons.add(
                lessonTitle.strip()
        );
    }

    public void submitForReview() {
        if (lessons.isEmpty()) {
            throw new IllegalStateException(
                    "Course must contain at least one lesson before review."
            );
        }

        changeStatus(
                CourseStatus.REVIEW
        );
    }

    public void returnToDraft() {
        changeStatus(
                CourseStatus.DRAFT
        );
    }

    public void publish() {
        changeStatus(
                CourseStatus.PUBLISHED
        );
    }

    public void archive() {
        changeStatus(
                CourseStatus.ARCHIVED
        );
    }

    private void changeStatus(
            CourseStatus targetStatus
    ) {
        if (
                !status.canTransitionTo(
                        targetStatus
                )
        ) {
            throw new InvalidCourseTransitionException(
                    status,
                    targetStatus
            );
        }

        status =
                targetStatus;
    }

    public CourseCode getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    public long getPriceInPaisa() {
        return priceInPaisa;
    }

    public CourseStatus getStatus() {
        return status;
    }

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

    @Override
    public String toString() {
        return code
                + " — "
                + title
                + " — "
                + status;
    }
}

Part 5: Request Validation

CreateCourseRequest.java

package io.liveklass.api;

public record CreateCourseRequest(
        String code,
        String title,
        Long priceInPaisa
) {
}

ValidationError.java

package io.liveklass.validation;

public record ValidationError(
        String field,
        String code,
        String message
) {
}

ValidationResult.java

package io.liveklass.validation;

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

public final class ValidationResult {

    private final List<ValidationError> errors;

    public ValidationResult() {
        this.errors =
                new ArrayList<>();
    }

    public void add(
            String field,
            String code,
            String message
    ) {
        errors.add(
                new ValidationError(
                        field,
                        code,
                        message
                )
        );
    }

    public boolean isValid() {
        return errors.isEmpty();
    }

    public List<ValidationError> getErrors() {
        return List.copyOf(
                errors
        );
    }
}

CreateCourseRequestValidator.java

package io.liveklass.validation;

import io.liveklass.api.CreateCourseRequest;

public final class CreateCourseRequestValidator {

    public ValidationResult validate(
            CreateCourseRequest request
    ) {
        ValidationResult result =
                new ValidationResult();

        if (request == null) {
            result.add(
                    "request",
                    "REQUEST_REQUIRED",
                    "Request is required."
            );

            return result;
        }

        if (
                request.code() == null
                || request.code().isBlank()
        ) {
            result.add(
                    "code",
                    "COURSE_CODE_REQUIRED",
                    "Course code is required."
            );
        }

        if (
                request.title() == null
                || request.title().isBlank()
        ) {
            result.add(
                    "title",
                    "COURSE_TITLE_REQUIRED",
                    "Course title is required."
            );
        }

        if (
                request.priceInPaisa() == null
        ) {
            result.add(
                    "priceInPaisa",
                    "COURSE_PRICE_REQUIRED",
                    "Course price is required."
            );
        } else if (
                request.priceInPaisa() < 0
        ) {
            result.add(
                    "priceInPaisa",
                    "COURSE_PRICE_NEGATIVE",
                    "Course price cannot be negative."
            );
        }

        return result;
    }
}

Why Use a Validation Result?

Request field failures expected।

A user may submit several invalid fields together।

A ValidationResult can return:

Invalid code
Blank title
Negative price

in one response।

Domain constructors still validate themselves, because request handlers are not the only callers।


Part 6: Repository

CourseRepository.java

package io.liveklass.repository;

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

public interface CourseRepository {

    boolean existsByCode(
            CourseCode courseCode
    );

    Course findByCode(
            CourseCode courseCode
    );

    void save(
            Course course
    );
}

InMemoryCourseRepository.java

package io.liveklass.repository;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.exceptions.CourseRepositoryException;

import java.util.LinkedHashMap;
import java.util.Map;

public final class InMemoryCourseRepository
        implements CourseRepository {

    private final Map<CourseCode, Course> coursesByCode;

    private boolean available;

    public InMemoryCourseRepository() {
        this.coursesByCode =
                new LinkedHashMap<>();

        this.available =
                true;
    }

    @Override
    public boolean existsByCode(
            CourseCode courseCode
    ) {
        ensureAvailable();

        return coursesByCode.containsKey(
                courseCode
        );
    }

    @Override
    public Course findByCode(
            CourseCode courseCode
    ) {
        ensureAvailable();

        return coursesByCode.get(
                courseCode
        );
    }

    @Override
    public void save(
            Course course
    ) {
        ensureAvailable();

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

    public void setAvailable(
            boolean available
    ) {
        this.available =
                available;
    }

    private void ensureAvailable() {
        if (!available) {
            IllegalStateException cause =
                    new IllegalStateException(
                            "Simulated storage outage."
                    );

            throw new CourseRepositoryException(
                    "Course repository is unavailable.",
                    cause
            );
        }
    }
}

The original cause is preserved।

Higher layers see:

CourseRepositoryException

instead of storage implementation details।


Part 7: Application Service

CourseService.java

package io.liveklass.service;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.exceptions.CourseNotFoundException;
import io.liveklass.exceptions.DuplicateCourseCodeException;
import io.liveklass.repository.CourseRepository;

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 Course createCourse(
            CourseCode courseCode,
            String title,
            long priceInPaisa
    ) {
        if (
                repository.existsByCode(
                        courseCode
                )
        ) {
            throw new DuplicateCourseCodeException(
                    courseCode
            );
        }

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

        repository.save(
                course
        );

        return course;
    }

    public Course requireCourse(
            CourseCode courseCode
    ) {
        Course course =
                repository.findByCode(
                        courseCode
                );

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

        return course;
    }

    public void addLesson(
            CourseCode courseCode,
            String lessonTitle
    ) {
        Course course =
                requireCourse(
                        courseCode
                );

        course.addLesson(
                lessonTitle
        );

        repository.save(
                course
        );
    }

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

        course.submitForReview();

        repository.save(
                course
        );
    }

    public Course publish(
            CourseCode courseCode
    ) {
        Course course =
                requireCourse(
                        courseCode
                );

        course.publish();

        repository.save(
                course
        );

        return course;
    }
}

Part 8: API Response Models

FieldError.java

package io.liveklass.api;

public record FieldError(
        String field,
        String code,
        String message
) {
}

ErrorResponse.java

package io.liveklass.api;

import java.util.List;

public record ErrorResponse(
        String code,
        String message,
        String correlationId,
        List<FieldError> fieldErrors
) {
    public ErrorResponse {
        fieldErrors =
                fieldErrors == null
                ? List.of()
                : List.copyOf(
                        fieldErrors
                );
    }
}

ApiResponse.java

package io.liveklass.api;

public record ApiResponse<T>(
        int status,
        T data,
        ErrorResponse error
) {
    public static <T> ApiResponse<T> success(
            int status,
            T data
    ) {
        return new ApiResponse<>(
                status,
                data,
                null
        );
    }

    public static <T> ApiResponse<T> failure(
            int status,
            ErrorResponse error
    ) {
        return new ApiResponse<>(
                status,
                null,
                error
        );
    }

    public boolean isSuccessful() {
        return status >= 200
                && status < 300;
    }
}

Part 9: Failure Boundary

CourseHandler.java

package io.liveklass.api;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.exceptions.CourseNotFoundException;
import io.liveklass.exceptions.CourseRepositoryException;
import io.liveklass.exceptions.DuplicateCourseCodeException;
import io.liveklass.exceptions.InvalidCourseTransitionException;
import io.liveklass.service.CourseService;
import io.liveklass.validation.CreateCourseRequestValidator;
import io.liveklass.validation.ValidationError;
import io.liveklass.validation.ValidationResult;

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

public final class CourseHandler {

    private final CourseService service;
    private final CreateCourseRequestValidator validator;

    public CourseHandler(
            CourseService service,
            CreateCourseRequestValidator validator
    ) {
        this.service = service;
        this.validator = validator;
    }

    public ApiResponse<Course> create(
            CreateCourseRequest request,
            String incomingCorrelationId
    ) {
        String correlationId =
                resolveCorrelationId(
                        incomingCorrelationId
                );

        ValidationResult validation =
                validator.validate(
                        request
                );

        if (!validation.isValid()) {
            return ApiResponse.failure(
                    400,
                    new ErrorResponse(
                            "VALIDATION_FAILED",
                            "The request contains invalid fields.",
                            correlationId,
                            toFieldErrors(
                                    validation.getErrors()
                            )
                    )
            );
        }

        try {
            Course course =
                    service.createCourse(
                            new CourseCode(
                                    request.code()
                            ),
                            request.title(),
                            request.priceInPaisa()
                    );

            return ApiResponse.success(
                    201,
                    course
            );
        } catch (
                DuplicateCourseCodeException exception
        ) {
            return failure(
                    409,
                    "DUPLICATE_COURSE_CODE",
                    "A course with this code already exists.",
                    correlationId
            );
        } catch (
                IllegalArgumentException exception
        ) {
            return failure(
                    400,
                    "INVALID_COURSE_DATA",
                    "The course data is invalid.",
                    correlationId
            );
        } catch (
                CourseRepositoryException exception
        ) {
            logFailure(
                    "Course creation repository failure",
                    correlationId,
                    exception
            );

            return failure(
                    503,
                    "COURSE_SERVICE_UNAVAILABLE",
                    "Course creation is temporarily unavailable.",
                    correlationId
            );
        } catch (
                RuntimeException exception
        ) {
            logFailure(
                    "Unexpected course creation failure",
                    correlationId,
                    exception
            );

            return failure(
                    500,
                    "INTERNAL_ERROR",
                    "The course could not be created.",
                    correlationId
            );
        }
    }

    public ApiResponse<Course> publish(
            String rawCourseCode,
            String incomingCorrelationId
    ) {
        String correlationId =
                resolveCorrelationId(
                        incomingCorrelationId
                );

        try {
            Course course =
                    service.publish(
                            new CourseCode(
                                    rawCourseCode
                            )
                    );

            return ApiResponse.success(
                    200,
                    course
            );
        } catch (
                IllegalArgumentException exception
        ) {
            return failure(
                    400,
                    "INVALID_COURSE_CODE",
                    "Enter a valid course code.",
                    correlationId
            );
        } catch (
                CourseNotFoundException exception
        ) {
            return failure(
                    404,
                    "COURSE_NOT_FOUND",
                    "The requested course was not found.",
                    correlationId
            );
        } catch (
                InvalidCourseTransitionException exception
        ) {
            return failure(
                    409,
                    "INVALID_COURSE_TRANSITION",
                    "The course cannot be published from its current state.",
                    correlationId
            );
        } catch (
                CourseRepositoryException exception
        ) {
            logFailure(
                    "Course publication repository failure",
                    correlationId,
                    exception
            );

            return failure(
                    503,
                    "COURSE_SERVICE_UNAVAILABLE",
                    "Course publication is temporarily unavailable.",
                    correlationId
            );
        } catch (
                RuntimeException exception
        ) {
            logFailure(
                    "Unexpected publication failure",
                    correlationId,
                    exception
            );

            return failure(
                    500,
                    "INTERNAL_ERROR",
                    "The course could not be published.",
                    correlationId
            );
        }
    }

    private ApiResponse<Course> failure(
            int status,
            String code,
            String message,
            String correlationId
    ) {
        return ApiResponse.failure(
                status,
                new ErrorResponse(
                        code,
                        message,
                        correlationId,
                        List.of()
                )
        );
    }

    private List<FieldError> toFieldErrors(
            List<ValidationError> errors
    ) {
        List<FieldError> result =
                new ArrayList<>();

        for (
                ValidationError error
                : errors
        ) {
            result.add(
                    new FieldError(
                            error.field(),
                            error.code(),
                            error.message()
                    )
            );
        }

        return List.copyOf(
                result
        );
    }

    private String resolveCorrelationId(
            String incomingCorrelationId
    ) {
        if (
                incomingCorrelationId != null
                && !incomingCorrelationId.isBlank()
        ) {
            return incomingCorrelationId.strip();
        }

        return UUID.randomUUID()
                .toString();
    }

    private void logFailure(
            String operation,
            String correlationId,
            RuntimeException exception
    ) {
        System.err.println(
                operation
                + ". correlationId="
                + correlationId
        );

        exception.printStackTrace(
                System.err
        );
    }
}

Boundary Mapping

FailureStatusCode
Invalid fields400VALIDATION_FAILED
Invalid course code400INVALID_COURSE_CODE
Course missing404COURSE_NOT_FOUND
Duplicate code409DUPLICATE_COURSE_CODE
Invalid transition409INVALID_COURSE_TRANSITION
Repository unavailable503COURSE_SERVICE_UNAVAILABLE
Unexpected failure500INTERNAL_ERROR

Part 10: Example Application

Main.java

package io.liveklass;

import io.liveklass.api.ApiResponse;
import io.liveklass.api.CourseHandler;
import io.liveklass.api.CreateCourseRequest;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.repository.InMemoryCourseRepository;
import io.liveklass.service.CourseService;
import io.liveklass.validation.CreateCourseRequestValidator;

public class Main {

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

        CourseService service =
                new CourseService(
                        repository
                );

        CourseHandler handler =
                new CourseHandler(
                        service,
                        new CreateCourseRequestValidator()
                );

        print(
                handler.create(
                        new CreateCourseRequest(
                                "",
                                "",
                                -10L
                        ),
                        "req-invalid"
                )
        );

        print(
                handler.create(
                        new CreateCourseRequest(
                                "java-oop",
                                "Java and OOP Foundation",
                                499_000L
                        ),
                        "req-create"
                )
        );

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

        service.addLesson(
                code,
                "Introduction to Java"
        );

        service.submitForReview(
                code
        );

        print(
                handler.publish(
                        "java-oop",
                        "req-publish"
                )
        );

        repository.setAvailable(
                false
        );

        print(
                handler.publish(
                        "java-oop",
                        "req-outage"
                )
        );
    }

    private static void print(
            ApiResponse<Course> response
    ) {
        System.out.println();

        System.out.println(
                "Status: "
                + response.status()
        );

        if (response.isSuccessful()) {
            System.out.println(
                    "Data: "
                    + response.data()
            );

            return;
        }

        System.out.println(
                "Code: "
                + response.error()
                        .code()
        );

        System.out.println(
                "Message: "
                + response.error()
                        .message()
        );

        System.out.println(
                "Correlation ID: "
                + response.error()
                        .correlationId()
        );

        for (
                var fieldError
                : response.error()
                        .fieldErrors()
        ) {
            System.out.println(
                    fieldError.field()
                    + ": "
                    + fieldError.message()
            );
        }
    }
}

Expected Behavior

Invalid request:

Status: 400
Code: VALIDATION_FAILED

Valid creation:

Status: 201

Successful publication:

Status: 200

Repository outage:

Status: 503
Code: COURSE_SERVICE_UNAVAILABLE

The outage stack trace is logged internally, not returned as response data।


Design Assessment

Why Use ValidationResult?

Field errors are expected and several may be returned together।

Why Throw from the Domain?

Invalid domain state should not be silently accepted।

Why Use Custom Exceptions?

The boundary needs to distinguish:

Not found
Duplicate
Invalid transition
Repository failure

Why Preserve Repository Cause?

Developers need the original storage failure for diagnosis।

Why Use a Correlation ID?

It connects the safe client response with internal logs।


Practice Tasks

Task 1: Add Archive Operation

Implement:

ApiResponse<Course> archive(
        String rawCourseCode,
        String correlationId
)

Expected mapping:

  • Missing course → 404
  • Invalid transition → 409
  • Repository failure → 503

Task 2: Create a Checked Import Exception

public final class CourseImportException
        extends Exception {
}

Use it in:

Course importCourse(
        Path path
) throws CourseImportException

Preserve an underlying IOException


Task 3: Add Error Code Enum

Replace raw code strings with:

ApiErrorCode

Use an explicit string field।

Do not serialize:

ordinal()

Task 4: Add Batch Result

Create a batch operation returning:

Success count
Created courses
Failed requests
Error code per failure

Decide whether processing continues after one failure।


Task 5: Add Idempotency

Course creation accepts:

idempotencyKey

Rules:

  • Same key and same request returns original result
  • Same key and different request returns conflict
  • Failed request must not be stored as success

Checked or Unchecked Assessment

Choose a suitable design।

Blank Course Title

IllegalArgumentException

Unchecked contract violation।

Required Import File Cannot Be Read

Possible checked:

CourseImportException

when every caller must choose retry or abort।

Repository Unavailable

Unchecked:

CourseRepositoryException

propagating to the boundary।

Optional Course Lookup Has No Result

Return:

null

or:

Optional<Course>

Publish from Invalid State

InvalidCourseTransitionException

or a structured result if rejection is normal workflow।


Find the Design Problem

Problem 1

catch (
        Exception exception
) {
    return failure(
            400,
            exception.getMessage()
    );
}

Problems:

  • Every failure becomes client error
  • Internal message may leak
  • Infrastructure failure is mislabeled
  • Catch is too broad

Problem 2

catch (
        SQLException exception
) {
    return null;
}

Problem:

Not found

and:

Query failed

become indistinguishable।


Problem 3

finally {
    return true;
}

Problem:

May override return values and suppress exceptions।


Problem 4

catch (
        IOException exception
) {
    throw new CourseImportException(
            "Import failed."
    );
}

Problem:

Original cause is lost।


Predict the Result

Question 1

try {
    throw new CourseNotFoundException(
            new CourseCode(
                    "JAVA"
            )
    );
} catch (
        RuntimeException exception
) {
    System.out.println(
            "Runtime"
    );
} catch (
        CourseNotFoundException exception
) {
    System.out.println(
            "Not found"
    );
}

Answer

The code does not compile।

The child exception catch is unreachable because the parent catch appears first।


Question 2

CourseRepositoryException exception =
        new CourseRepositoryException(
                "Repository failed.",
                new IllegalStateException(
                        "Connection unavailable."
                )
        );

System.out.println(
        exception.getCause()
                .getMessage()
);

Answer

Connection unavailable.

Question 3

Can three field validation errors be returned without three exceptions?

Answer

Yes।

Use one structured ValidationResult containing three errors।


Question 4

Should a repository timeout return COURSE_NOT_FOUND?

Answer

No।

The operation failed; it did not successfully determine that the course is absent।


Concept Assessment

Determine True or False.

  1. Checked exceptions must be caught or declared.
  2. Every business rejection should be an exception.
  3. RuntimeException is unchecked.
  4. Validation results can contain multiple errors.
  5. Messages should be parsed as API codes.
  6. Causes should be preserved when wrapping.
  7. Stack traces should be returned to clients.
  8. Not-found and repository failure are equivalent.
  9. Specific catches must appear before parent catches.
  10. Correlation IDs connect responses to logs.
  11. Database constraints may still be required.
  12. Returning from finally can hide exceptions.

Concept Assessment Answers

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

Final Challenge

Build a course-management failure flow with:

  • Request validation
  • Multiple field errors
  • Strong CourseCode
  • Domain state transitions
  • Duplicate detection
  • Required course lookup
  • Repository exception translation
  • Cause preservation
  • Stable API error codes
  • Correlation IDs
  • Safe messages
  • Generic unexpected-failure fallback

Evaluation Rubric

AreaScore
Enum state model/2
Constructor validation/2
Multiple validation errors/2
Appropriate built-in exceptions/2
Meaningful custom exceptions/2
Cause preservation/2
Repository translation/2
Specific catch ordering/2
Stable error codes/2
Safe external messages/2
Correlation ID support/2
Unexpected failure fallback/2

Maximum:

24

Interpretation:

21–24 → Strong understanding
17–20 → Good foundation
12–16 → Review failure contracts
Below 12 → Rebuild the project

Module Completion Checklist

  • Use enum for fixed domain states
  • Use throw and throws
  • Write specific try-catch
  • Order catches correctly
  • Avoid returning from finally
  • Distinguish checked and unchecked exceptions
  • Preserve original causes
  • Create meaningful custom exceptions
  • Use validation results for multiple errors
  • Translate repository failures
  • Separate not-found from infrastructure failure
  • Return stable error codes
  • Hide internal details
  • Include correlation IDs
  • Handle unexpected failures safely

Module Summary

এই module-এ আমরা শিখেছি:

  • enum fixed domain states model করে
  • Invalid transitions domain rules দিয়ে reject করা যায়
  • Exceptions typed failure objects
  • throw exceptional flow শুরু করে
  • throws possible failure declare করে
  • Checked exceptions catch বা declare করতে হয়
  • Unchecked exceptions compiler-enforced নয়
  • Specific catches broad catches-এর আগে থাকে
  • finally cleanup করতে পারে, কিন্তু return করা dangerous
  • Try-with-resources resources safely close করে
  • Custom exceptions meaningful failure categories তৈরি করে
  • Original cause preserve করা debugging-এর জন্য essential
  • Validation results multiple expected field errors represent করে
  • Domain validation invalid objects prevent করে
  • Database constraints concurrent consistency protect করে
  • Failure boundaries internal exceptionsকে safe responses-এ translate করে
  • Stable error codes clientsকে predictable contract দেয়
  • Correlation IDs responses এবং logs connect করে
  • Not-found, conflict, infrastructure, এবং unexpected failures আলাদা রাখা উচিত
  • Reliable failure handling syntax-এর চেয়ে বেশি; এটি contract design

Module Complete

প্রতিটি failure-এর জন্য জিজ্ঞেস করুন:

Is this expected?
Which layer understands it?
Should it be a result or exception?
Where should it be translated and logged?

এই questions strong failure-handling design-এর foundation।