Enums, Exceptions, and Robust Error Handling
Practice and Assessment
আপনি একটি 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
| Failure | Status | Code |
|---|---|---|
| Invalid fields | 400 | VALIDATION_FAILED |
| Invalid course code | 400 | INVALID_COURSE_CODE |
| Course missing | 404 | COURSE_NOT_FOUND |
| Duplicate code | 409 | DUPLICATE_COURSE_CODE |
| Invalid transition | 409 | INVALID_COURSE_TRANSITION |
| Repository unavailable | 503 | COURSE_SERVICE_UNAVAILABLE |
| Unexpected failure | 500 | INTERNAL_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.
- Checked exceptions must be caught or declared.
- Every business rejection should be an exception.
RuntimeExceptionis unchecked.- Validation results can contain multiple errors.
- Messages should be parsed as API codes.
- Causes should be preserved when wrapping.
- Stack traces should be returned to clients.
- Not-found and repository failure are equivalent.
- Specific catches must appear before parent catches.
- Correlation IDs connect responses to logs.
- Database constraints may still be required.
- Returning from
finallycan 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
| Area | Score |
|---|---|
| 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
throwandthrows - 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-এ আমরা শিখেছি:
enumfixed domain states model করে- Invalid transitions domain rules দিয়ে reject করা যায়
- Exceptions typed failure objects
throwexceptional flow শুরু করেthrowspossible failure declare করে- Checked exceptions catch বা declare করতে হয়
- Unchecked exceptions compiler-enforced নয়
- Specific catches broad catches-এর আগে থাকে
finallycleanup করতে পারে, কিন্তু 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।