Enums, Exceptions, and Robust Error Handling

Building Reliable Failure Boundaries

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

Domain object এবং application service failure detect করতে পারে।

কিন্তু end user বা API clientকে raw Java exception দেওয়া উচিত নয়।

Example internal failure:

CourseRepositoryException
Caused by: SQLException
Connection timed out

Client-এর প্রয়োজন হতে পারে:

{
  "code": "COURSE_SERVICE_UNAVAILABLE",
  "message": "Course information is temporarily unavailable."
}

যে layer internal failureকে external response-এ translate করে, সেটি একটি failure boundary।

Common boundaries:

HTTP API
Message consumer
Scheduled job
Command-line application
Background worker
User interface

একটি reliable boundary:

  • Known failures classify করে
  • Safe response তৈরি করে
  • Unexpected failures log করে
  • Stable error codes return করে
  • Internal details hide করে
  • Request correlation preserve করে
  • Retryable এবং non-retryable failure distinguish করে
  • Partial success নিয়ে deliberate decision নেয়
  • Duplicate logging avoid করে
  • Transaction এবং idempotency consider করে

Learning Objectives

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

  • Failure boundary explain করতে
  • Domain, validation, authorization, এবং infrastructure failures map করতে
  • Consistent error response design করতে
  • Safe user message এবং internal log আলাদা রাখতে
  • Correlation ID-এর purpose বুঝতে
  • Retryable failure identify করতে
  • Partial failure handle করতে
  • Idempotent operation design করতে
  • Transaction boundary reason করতে
  • Duplicate exception logging avoid করতে
  • Unknown exception safely handle করতে

What Is an Application Boundary?

An application boundary হলো এমন জায়গা যেখানে internal application behavior external caller-এর সঙ্গে interact করে।

Example HTTP flow:

HTTP Request
↓
API Handler
↓
Application Service
↓
Domain and Repository

Response flow:

Internal result or exception
↓
API Handler
↓
HTTP Response

API handler failure boundary হিসেবে কাজ করতে পারে।


Why Boundaries Matter

Without a deliberate boundary:

  • Stack trace client-এর কাছে leak হতে পারে
  • Different endpoints different error format দিতে পারে
  • Database failure not-found হিসেবে return হতে পারে
  • Client retry করা উচিত কি না বুঝতে পারে না
  • Same exception multiple layers-এ log হতে পারে
  • Sensitive infrastructure details expose হতে পারে
  • Unexpected failures inconsistentভাবে handled হতে পারে

Boundary internal complexityকে stable external contract-এ translate করে।


Internal Failure Categories

A practical classification:

Validation failure
Authentication failure
Authorization failure
Not-found failure
Conflict or duplicate failure
Invalid domain state
Rate-limit failure
Infrastructure failure
Unexpected programming failure

Each category may require different response।


Example HTTP Mapping

Internal conditionPossible HTTP status
Invalid request fields400 Bad Request
Authentication required401 Unauthorized
Access denied403 Forbidden
Course not found404 Not Found
Duplicate course code409 Conflict
Invalid state transition409 Conflict
Rate limit exceeded429 Too Many Requests
Repository unavailable500 or 503
Unexpected failure500 Internal Server Error

Exact mapping depends on API conventions।

Consistency is more important than blindly following one table।


A Consistent Error Response

import java.util.List;

public record ErrorResponse(
        String code,
        String message,
        String correlationId,
        List<FieldError> fieldErrors
) {
    public ErrorResponse {
        if (
                code == null
                || code.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Error code is required."
            );
        }

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

        fieldErrors =
                fieldErrors == null
                ? List.of()
                : List.copyOf(
                        fieldErrors
                );
    }
}

Field error:

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

Example Validation Response

{
  "code": "VALIDATION_FAILED",
  "message": "The request contains invalid fields.",
  "correlationId": "req-3a712",
  "fieldErrors": [
    {
      "field": "title",
      "code": "COURSE_TITLE_REQUIRED",
      "message": "Course title is required."
    }
  ]
}

Stable Code vs Human Message

Error Code

COURSE_NOT_FOUND
DUPLICATE_COURSE_CODE
COURSE_SERVICE_UNAVAILABLE

Used by:

  • Frontend logic
  • Mobile client
  • Tests
  • Monitoring
  • Localization

Message

The requested course was not found.

Used by humans।

Messages may change or be translated।

Clients should not parse messages programmatically।


Safe User Messages

Internal exception:

CourseRepositoryException:
Query failed against postgresql.internal:5432

Unsafe external response:

Query failed against postgresql.internal:5432

Safe response:

Course information is temporarily unavailable.

Internal logs can preserve technical detail।


Do Not Expose Stack Traces

Never return:

java.lang.NullPointerException
at io.liveklass...

to clients।

Risks:

  • Internal class names exposed
  • File paths exposed
  • Framework details exposed
  • Security weaknesses revealed
  • Poor user experience

Stack traces belong in secure diagnostic systems।


Correlation ID

A correlation ID uniquely identifies one request or operation across logs and services।

Example:

req-3a712

Client receives:

{
  "code": "COURSE_CREATION_FAILED",
  "message": "The course could not be created.",
  "correlationId": "req-3a712"
}

Support engineer can search logs using:

req-3a712

Why Correlation IDs Help

A user may report:

I received an error while publishing.

Without request identity, finding the exact failure is difficult।

With correlation ID:

Please provide correlation ID req-3a712.

Logs can connect:

API request
Application service
Repository call
External payment call
Final response

Correlation ID Generation

A simple example:

String correlationId =
        UUID.randomUUID()
                .toString();

In real systems, an existing incoming correlation ID may be accepted and propagated।

Rules:

  • Validate incoming format
  • Generate one if absent
  • Include it in logs
  • Include safe value in response
  • Propagate to downstream requests

Logging Context

Weak log:

Failed to publish course.

Better:

Failed to publish course.
correlationId=req-3a712
courseCode=JAVA-OOP
instructorId=4001

Useful context may include:

  • Correlation ID
  • Operation name
  • Safe entity identifiers
  • Current state
  • Exception stack trace

Avoid secrets and unnecessary personal data।


Log Levels

A simplified mental model:

DEBUG

Detailed development or diagnostic information।

INFO

Normal important lifecycle events।

Course published successfully

WARN

Unexpected but potentially recoverable situation।

External service timed out; retry scheduled

ERROR

Operation failed and needs investigation।

Course repository unavailable

Do not log every expected validation failure as an error।


Validation Failure Is Usually Not a Server Error

User submits blank title।

This is expected input failure।

Possible handling:

HTTP 400
No exception stack trace at ERROR level
Structured field response

Logging every bad request with a stack trace creates noise।

Security-related suspicious patterns may still deserve separate monitoring।


Log Once at the Owning Boundary

Weak flow:

Repository logs exception
Service logs same exception
Handler logs same exception
Global handler logs same exception

One failure creates four stack traces।

Better:

  • Lower layer adds context through exception wrapping
  • Final responsible boundary logs once
  • Expected failures may not require stack traces

When Lower Layers Should Log

Lower layer may log when it owns a unique event that outer layers cannot reconstruct।

Examples:

  • Retry attempt count
  • Circuit breaker opened
  • Batch item skipped intentionally
  • External provider response metadata

But avoid logging and rethrowing identical failure without new value।


Expected vs Unexpected Failures

Expected

Invalid field
Course not found
Duplicate enrollment
Access denied

Boundary has explicit mapping।

Unexpected

NullPointerException
ArrayIndexOutOfBoundsException
Unknown RuntimeException

Boundary should:

  • Log full exception
  • Return generic safe response
  • Include correlation ID
  • Avoid pretending it is validation or not-found

Generic Fallback Handler

Conceptually:

catch (
        RuntimeException exception
) {
    logger.error(
            "Unexpected request failure. correlationId={}",
            correlationId,
            exception
    );

    return serverError(
            new ErrorResponse(
                    "INTERNAL_ERROR",
                    "The request could not be completed.",
                    correlationId,
                    List.of()
            )
    );
}

A broad catch is appropriate at a top-level boundary as a final safety net।

It is usually inappropriate throughout internal business code।


Catch Ordering at a Boundary

try {
    return service.publish(
            courseCode
    );
} catch (
        CourseNotFoundException exception
) {
    // 404
} catch (
        InvalidCourseTransitionException exception
) {
    // 409
} catch (
        CourseRepositoryException exception
) {
    // 503 or 500
} catch (
        RuntimeException exception
) {
    // Unexpected 500
}

Specific catches first, generic fallback last।


Complete Boundary Example

public final class CourseRequestHandler {

    private final CourseService courseService;
    private final Logger logger;

    public CourseRequestHandler(
            CourseService courseService,
            Logger logger
    ) {
        if (courseService == null) {
            throw new IllegalArgumentException(
                    "Course service is required."
            );
        }

        if (logger == null) {
            throw new IllegalArgumentException(
                    "Logger is required."
            );
        }

        this.courseService =
                courseService;

        this.logger =
                logger;
    }

    public ApiResponse<Course> publish(
            String rawCourseCode,
            String correlationId
    ) {
        try {
            CourseCode courseCode =
                    new CourseCode(
                            rawCourseCode
                    );

            Course course =
                    courseService.publish(
                            courseCode
                    );

            return ApiResponse.success(
                    200,
                    course
            );
        } catch (
                IllegalArgumentException exception
        ) {
            return ApiResponse.failure(
                    400,
                    new ErrorResponse(
                            "INVALID_COURSE_CODE",
                            "Enter a valid course code.",
                            correlationId,
                            List.of()
                    )
            );
        } catch (
                CourseNotFoundException exception
        ) {
            return ApiResponse.failure(
                    404,
                    new ErrorResponse(
                            "COURSE_NOT_FOUND",
                            "The requested course was not found.",
                            correlationId,
                            List.of()
                    )
            );
        } catch (
                InvalidCourseTransitionException exception
        ) {
            return ApiResponse.failure(
                    409,
                    new ErrorResponse(
                            "COURSE_CANNOT_BE_PUBLISHED",
                            "The course cannot be published from its current state.",
                            correlationId,
                            List.of()
                    )
            );
        } catch (
                CourseRepositoryException exception
        ) {
            logger.error(
                    "Course repository failure. correlationId="
                    + correlationId,
                    exception
            );

            return ApiResponse.failure(
                    503,
                    new ErrorResponse(
                            "COURSE_SERVICE_UNAVAILABLE",
                            "Course information is temporarily unavailable.",
                            correlationId,
                            List.of()
                    )
            );
        } catch (
                RuntimeException exception
        ) {
            logger.error(
                    "Unexpected publication failure. correlationId="
                    + correlationId,
                    exception
            );

            return ApiResponse.failure(
                    500,
                    new ErrorResponse(
                            "INTERNAL_ERROR",
                            "The request could not be completed.",
                            correlationId,
                            List.of()
                    )
            );
        }
    }
}

ApiResponse এখানে simplified framework-independent model।


Do Not Catch Errors You Cannot Translate Correctly

Weak:

catch (
        RuntimeException exception
) {
    return notFound();
}

A NullPointerException is not a not-found result।

Generic fallback must map to generic internal failure, not a misleading specific category।


Retryable Failures

Some failures may succeed if attempted again।

Examples:

Temporary network timeout
Service unavailable
Rate limit with retry delay
Transient database connection failure

Possible retryable response:

503 Service Unavailable

or:

429 Too Many Requests

with retry guidance।


Non-Retryable Failures

Examples:

Invalid course code
Access denied
Duplicate enrollment
Unsupported status transition

Retrying the exact same request without changing anything will not help।

Client should fix input, permissions, or state।


Retryability Is Contextual

A database timeout may be retryable।

A database unique constraint violation is not transient।

Both may originate from database operations, but require different treatment।

Do not mark every infrastructure exception retryable।


Retry Only Safe Operations

Before retrying, ask:

Can the operation be repeated without creating duplicate effects?

A read operation is often safer to retry।

A payment charge or enrollment creation may not be safe unless idempotency is designed।


Idempotency

An operation is idempotent when repeating the same request produces the same intended effect without duplicate side effects।

Example:

Set course title to "Java"

Repeating it still results in:

Title = Java

Non-idempotent example:

Add 100 credits

Repeating it adds 200 credits।


Idempotency Key

For a sensitive create operation, client may send:

idempotencyKey=payment-req-7821

Server stores the result associated with that key।

If the same request retries:

  • Do not create another charge
  • Return the original result

Useful for:

Payments
Orders
Enrollments
External provisioning

Duplicate Enrollment and Idempotency

Suppose enrollment request times out after server successfully saves it।

Client does not know whether it succeeded and retries।

Without idempotency:

Second request may fail as duplicate

With idempotent semantics:

Already enrolled in the same course

may return the existing successful result।

The best response depends on API contract।


Retry Strategy

A retry strategy may include:

  • Maximum attempts
  • Delay
  • Exponential backoff
  • Random jitter
  • Retryable exception list
  • Timeout
  • Idempotency guarantee

Avoid immediate infinite retries:

while (true) {
    callService();
}

This can worsen an outage।


Exponential Backoff

Concept:

Attempt 1 → Wait 1 second
Attempt 2 → Wait 2 seconds
Attempt 3 → Wait 4 seconds

Jitter adds small randomness so many clients do not retry simultaneously।

Detailed resilience patterns are advanced topics, but retry should always be bounded and deliberate।


Partial Failure

Suppose course publication performs:

  1. Update course status
  2. Save database record
  3. Send email
  4. Publish analytics event

What if:

Database save succeeds
Email fails

Was the entire operation unsuccessful?

This is a partial failure।


Identify the Primary Transaction

Ask:

What is the core business effect?

For course publication:

Course status persisted as PUBLISHED

Email may be a secondary side effect।

Possible design:

  • Commit course publication
  • Record notification task
  • Retry email separately
  • Do not roll back publication only because email failed

Do Not Pretend Atomicity Across Independent Systems

Database transaction cannot usually atomically include:

Database
Email provider
Analytics service
Message broker
Payment provider

If one external system fails, partial completion is possible।

Design must acknowledge this।


Transaction Boundary

A transaction groups operations that should commit or roll back together within supported storage।

Example:

Insert enrollment
Update available seats
Write enrollment audit record

If all are in one database, one transaction may protect consistency।

Either:

All commit

or:

All roll back

Keep Transactions Focused

Avoid holding a database transaction open while:

  • Calling a slow external API
  • Sending email
  • Uploading a large file
  • Waiting for user interaction
  • Sleeping for retries

Long transactions can:

  • Hold locks
  • Reduce throughput
  • Increase deadlocks
  • Increase rollback cost

Transaction Rollback and Exceptions

Many frameworks roll back transactions when an unchecked exception escapes।

But exact behavior depends on framework configuration।

Do not assume every caught exception triggers rollback।

Example:

try {
    repository.save(
            course
    );
} catch (
        CourseRepositoryException exception
) {
    return false;
}

If the exception is swallowed inside the transaction, framework may see normal completion।

Transaction behavior must be understood explicitly।


Do Not Catch Only to Force Success

Weak:

public boolean createEnrollment() {
    try {
        saveEnrollment();
        updateSeatCount();

        return true;
    } catch (
        RuntimeException exception
    ) {
        return false;
    }
}

The caller receives no reason, and transaction semantics may become unclear।

Let the appropriate failure propagate to the transaction boundary unless a valid local recovery exists।


Side Effects After Commit

A safer sequence may be:

1. Validate
2. Start transaction
3. Persist core state
4. Commit transaction
5. Trigger secondary effect

But if step 5 fails, it needs retry or durable tracking।

Simply calling email after commit without recording pending work can lose the notification।


Transactional Outbox: High-Level Idea

A common reliable pattern:

Within one database transaction:
- Save course publication
- Save "CoursePublished" outbox event

A separate worker later:

Reads outbox event
Sends notification
Marks event processed

This reduces the gap between database state and event publishing।

Detailed implementation is an advanced distributed-systems topic।


Batch Processing Failure Strategies

Suppose a job enrolls 1,000 learners।

Possible strategies:

Fail Entire Batch

One failure → Roll back everything

Useful when all records form one atomic unit।

Continue and Collect Failures

Successful items remain
Failed items reported separately

Useful when items are independent।

Stop at First Failure

Simple but may leave partial work।

The strategy must be explicit।


Batch Result Model

public record BatchFailure<T>(
        T item,
        String code,
        String message
) {
}
public record BatchResult<T>(
        int successCount,
        List<BatchFailure<T>> failures
) {
    public BatchResult {
        failures =
                failures == null
                ? List.of()
                : List.copyOf(
                        failures
                );
    }
}

This communicates partial success clearly।


Do Not Return Only false for a Batch

boolean imported

cannot explain:

How many succeeded?
Which items failed?
Can failures be retried?
Was anything committed?

Use structured results when partial completion is possible।


Message Consumer Boundaries

A background message consumer also needs failure policy।

Possible outcomes:

Acknowledge message
Retry later
Move to dead-letter queue
Reject permanently

Example classification:

Invalid message format → Dead-letter
Temporary database failure → Retry
Already processed event → Acknowledge
Unexpected bug → Retry with limit, then dead-letter

Poison Messages

A poison message always fails because its content is permanently invalid।

Infinite retry wastes resources।

Examples:

Missing required event field
Unsupported event version
Invalid identifier

After limited attempts, move it to a dead-letter queue or failure store for investigation।


Scheduled Job Boundaries

A scheduled job should report:

  • Start time
  • Completion
  • Processed count
  • Failed count
  • Correlation or execution ID
  • Final status
  • Exception details for unexpected failure

Do not silently stop after the first exception without recording job outcome।


Cancellation and Interruption

When catching:

InterruptedException

restore interruption:

catch (
        InterruptedException exception
) {
    Thread.currentThread()
            .interrupt();

    throw new JobInterruptedException(
            "Course import was interrupted.",
            exception
    );
}

A boundary can mark the operation cancelled rather than failed if that distinction matters।


Timeout Is a Failure Contract

External calls should not wait forever।

A reliable boundary should understand:

Connection timeout
Read timeout
Overall operation deadline

Timeout failure may map to:

SERVICE_TIMEOUT

and may be retryable if the operation is safe।


Error Response Should Not Promise What You Do Not Know

Weak message after timeout:

Enrollment failed.

The operation may actually have succeeded remotely before the response was lost।

Safer message:

We could not confirm the enrollment result.

Then use:

  • Idempotency key
  • Status lookup
  • Reconciliation
  • Safe retry

This distinction is critical for payments and external systems।


Observability Beyond Logs

Reliable failure boundaries can emit:

  • Metrics
  • Traces
  • Structured logs
  • Alerts

Example metrics:

course_publish_success_total
course_publish_failure_total
course_repository_timeout_total

Failures should be measurable, not only printable।


Avoid High-Cardinality Metric Labels

Unsafe metric label:

learnerId
courseId
correlationId

Millions of unique values can overload metrics systems।

Use identifiers in logs or traces।

Metrics labels should use bounded categories:

errorType
operation
status
provider

A Reliable Boundary Checklist

For each operation ask:

  1. What are expected failures?
  2. What are unexpected failures?
  3. Which failures should be logged?
  4. Which failures are retryable?
  5. Is retry safe?
  6. Is the operation idempotent?
  7. Can partial success occur?
  8. What is the transaction boundary?
  9. What error code should the caller receive?
  10. What internal details must remain hidden?
  11. How will support find the request?
  12. What metrics should be recorded?

Complete Example: Enrollment Boundary

Error Codes

public enum EnrollmentErrorCode {

    INVALID_REQUEST,
    COURSE_NOT_FOUND,
    ALREADY_ENROLLED,
    ACCESS_DENIED,
    SERVICE_UNAVAILABLE,
    INTERNAL_ERROR
}

Enrollment Response

public record EnrollmentResponse(
        boolean successful,
        Enrollment enrollment,
        ErrorResponse error
) {
    public static EnrollmentResponse success(
            Enrollment enrollment
    ) {
        return new EnrollmentResponse(
                true,
                enrollment,
                null
        );
    }

    public static EnrollmentResponse failure(
            ErrorResponse error
    ) {
        return new EnrollmentResponse(
                false,
                null,
                error
        );
    }
}

Boundary Handler

public final class EnrollmentHandler {

    private final EnrollmentService service;
    private final Logger logger;

    public EnrollmentHandler(
            EnrollmentService service,
            Logger logger
    ) {
        this.service =
                service;

        this.logger =
                logger;
    }

    public EnrollmentResponse enroll(
            long learnerId,
            String rawCourseCode,
            String correlationId
    ) {
        try {
            if (learnerId <= 0) {
                throw new IllegalArgumentException(
                        "Learner ID must be positive."
                );
            }

            CourseCode courseCode =
                    new CourseCode(
                            rawCourseCode
                    );

            Enrollment enrollment =
                    service.enroll(
                            learnerId,
                            courseCode
                    );

            return EnrollmentResponse.success(
                    enrollment
            );
        } catch (
                IllegalArgumentException exception
        ) {
            return failure(
                    "INVALID_REQUEST",
                    "The enrollment request is invalid.",
                    correlationId
            );
        } catch (
                CourseNotFoundException exception
        ) {
            return failure(
                    "COURSE_NOT_FOUND",
                    "The requested course was not found.",
                    correlationId
            );
        } catch (
                DuplicateEnrollmentException exception
        ) {
            return failure(
                    "ALREADY_ENROLLED",
                    "The learner is already enrolled in this course.",
                    correlationId
            );
        } catch (
                AccessDeniedException exception
        ) {
            return failure(
                    "ACCESS_DENIED",
                    "You are not allowed to perform this action.",
                    correlationId
            );
        } catch (
                EnrollmentRepositoryException exception
        ) {
            logger.error(
                    "Enrollment repository failure. correlationId="
                    + correlationId,
                    exception
            );

            return failure(
                    "SERVICE_UNAVAILABLE",
                    "Enrollment is temporarily unavailable.",
                    correlationId
            );
        } catch (
                RuntimeException exception
        ) {
            logger.error(
                    "Unexpected enrollment failure. correlationId="
                    + correlationId,
                    exception
            );

            return failure(
                    "INTERNAL_ERROR",
                    "The enrollment request could not be completed.",
                    correlationId
            );
        }
    }

    private EnrollmentResponse failure(
            String code,
            String message,
            String correlationId
    ) {
        return EnrollmentResponse.failure(
                new ErrorResponse(
                        code,
                        message,
                        correlationId,
                        List.of()
                )
        );
    }
}

Design Review

Why Are Validation Failures Not Logged as Errors?

They are expected caller mistakes, not server defects।

Why Is Repository Failure Logged?

It represents operational failure requiring diagnosis।

Why Is There a Final Runtime Catch?

It prevents raw unexpected exceptions from escaping the external boundary।

Why Is the Message Generic for Unexpected Failure?

The boundary does not know whether internal details are safe।

Why Include Correlation ID?

The response can be connected to internal logs।


Common Mistakes

Returning Raw Exception Messages

May expose internal or sensitive details।


Returning Stack Traces

Leaks implementation information।


Mapping Every Exception to 400

Programming and infrastructure failures are not invalid client input।


Mapping Every Exception to 500

Expected not-found and conflict failures lose meaning।


Logging Every Expected Validation Error at ERROR

Creates operational noise।


Logging the Same Exception at Every Layer

Creates duplicate stack traces।


Retrying Non-Idempotent Operations Blindly

May duplicate charges, enrollments, or notifications।


Retrying Permanent Validation Failures

The same request will continue failing।


Infinite Immediate Retry

Can worsen service outages।


Pretending a Timeout Means Definite Failure

Remote operation may have completed।


Treating Secondary Notification Failure as Core Transaction Failure

May roll back valid business state unnecessarily।


Holding Transactions Open During Slow External Calls

Increases lock time and failure risk।


Returning Only Boolean for Partial Batch Results

Hides committed work and failed items।


Swallowing Exceptions Inside Transactional Methods

May accidentally commit partial state।


Using Correlation IDs as Metric Labels

Creates excessive metric cardinality।


Practice Exercises

Exercise 1: Error Mapping

Map these failures to stable codes and possible HTTP statuses:

  1. Blank course code
  2. Course not found
  3. Duplicate course code
  4. Access denied
  5. Database timeout
  6. Unexpected NullPointerException

Exercise 2: Safe Messages

Create separate internal and external messages for:

  1. Database connection failure
  2. File permission failure
  3. Payment provider timeout
  4. Invalid learner ID

Exercise 3: Correlation ID

Design a flow that:

  • Reads incoming correlation ID
  • Generates one when absent
  • Adds it to logs
  • Returns it in error responses
  • Sends it to downstream services

Exercise 4: Retry Decision

Decide whether each failure is retryable:

  1. Invalid course code
  2. Temporary network timeout
  3. Duplicate enrollment
  4. Rate limit response
  5. Access denied
  6. Temporary database connection failure

Explain whether retry is safe।


Exercise 5: Idempotency

Design an idempotent enrollment endpoint using an idempotency key।

Explain:

  • Where the key is stored
  • What happens on repeated request
  • What response is returned
  • How conflicting payloads with the same key are handled

Exercise 6: Partial Failure

Course publication performs:

Save status
Send email
Publish analytics event

Design behavior when:

  • Save succeeds
  • Email fails
  • Analytics succeeds

Define the primary operation and retry strategy।


Exercise 7: Batch Import

Create a batch result that reports:

  • Successful course count
  • Failed source files
  • Error code per failure
  • Whether failed items are retryable

Exercise 8: Duplicate Logging

A repository, service, and API handler all log the same exception।

Choose one owning layer for the stack trace and explain what the other layers should do instead।


Predict the Result

Question 1

A request throws CourseNotFoundException and the boundary has a specific catch before RuntimeException.

Which catch executes?

Answer

The CourseNotFoundException catch executes।

Java selects the first compatible catch।


Question 2

A database timeout occurs, but boundary returns:

COURSE_NOT_FOUND

Is this correct?

Answer

No।

A failed lookup operation is different from a successful lookup with no result।


Question 3

A client retries a payment request after timeout without an idempotency key।

What risk exists?

Answer

The payment may be charged more than once if the first request actually succeeded।


Question 4

The database transaction commits course publication, then email sending fails।

Has the course necessarily failed to publish?

Answer

No।

The primary database state may already be committed।

Email failure is a secondary partial failure that may require retry।


Question 5

Should an invalid field response include a full exception stack trace?

Answer

No।

It should return structured validation errors without internal stack details।


Knowledge Check

Question 1

What is a failure boundary?

Question 2

Why use stable error codes?

Question 3

What is the difference between internal and external error messages?

Question 4

What is a correlation ID?

Question 5

Why should expected validation failures not usually be logged as server errors?

Question 6

Where should an unexpected exception usually be logged?

Question 7

What makes a failure retryable?

Question 8

What is idempotency?

Question 9

Why are retries dangerous for non-idempotent operations?

Question 10

What is a partial failure?

Question 11

What is a transaction boundary?

Question 12

Why should transactions avoid slow external calls?

Question 13

Why is a structured batch result better than a boolean?

Question 14

What is a poison message?

Question 15

Why should correlation IDs not be metric labels?


Knowledge Check Answers

Answer 1

A layer that converts internal results and failures into a stable external contract।

Answer 2

Clients can handle failure categories without parsing changeable messages।

Answer 3

Internal messages contain diagnostic detail; external messages must remain safe and user-appropriate।

Answer 4

A request or operation identifier used to connect responses, logs, traces, and downstream calls।

Answer 5

They are expected caller mistakes and logging every one as ERROR creates noise।

Answer 6

At the outer boundary that owns failure reporting and has sufficient request context।

Answer 7

The cause is temporary and repeating the operation has a realistic chance of success।

Answer 8

Repeating the same operation produces the same intended effect without duplicate side effects।

Answer 9

A repeated request may duplicate payments, enrollments, messages, or other effects।

Answer 10

Some parts of a multi-step operation succeed while other parts fail।

Answer 11

The set of storage operations that should commit or roll back together।

Answer 12

They hold locks longer, reduce throughput, and increase timeout and rollback risk।

Answer 13

It communicates success count, failed items, reasons, and retryability।

Answer 14

A permanently invalid message that will fail on every retry।

Answer 15

They are highly unique and can create excessive metrics cardinality।


Lesson Summary

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

  • Failure boundary internal failuresকে external contracts-এ translate করে
  • HTTP API, jobs, consumers, and CLIs সব failure boundaries হতে পারে
  • Validation, authorization, not-found, conflict, infrastructure, and unexpected failures আলাদা categories
  • Stable error codes programmatic client handling support করে
  • Human messages change বা localize হতে পারে
  • Raw stack traces এবং infrastructure details clientsকে expose করা উচিত নয়
  • Correlation IDs requests এবং logs connect করে
  • Safe entity context logging diagnosis improve করে
  • Expected validation failures সাধারণত server errors হিসেবে log করা উচিত নয়
  • Unexpected and infrastructure failures owning boundary-তে log করা উচিত
  • Same exception every layer-এ log করলে duplicate noise তৈরি হয়
  • Specific catches generic fallback-এর আগে থাকে
  • Generic outer fallback raw exceptions escaping prevent করে
  • Retryable failures temporary; permanent input failures retryable নয়
  • Retry safety operation idempotency-এর ওপর depend করে
  • Idempotency keys duplicate side effects prevent করতে পারে
  • Timeouts outcome uncertain করতে পারে
  • Partial failure multi-step operations-এ common
  • Primary business effect এবং secondary side effects আলাদা করতে হয়
  • Transactions supported storage operationsকে atomically group করে
  • Slow external calls transaction-এর মধ্যে রাখা risky
  • Side effects after commit durable retry strategy require করতে পারে
  • Transactional outbox database state এবং event delivery coordinate করতে সাহায্য করে
  • Batch operations explicit all-or-nothing বা partial-success strategy require করে
  • Poison messages infinite retry করা উচিত নয়
  • Failure metrics bounded categories ব্যবহার করা উচিত
  • Reliable boundaries predictable, observable, safe, and consistent failure behavior তৈরি করে

Next Lesson

পরবর্তী lesson:

Module Practice and Assessment

আমরা তৈরি করব:

  • Course creation request validation
  • Domain invariants
  • Custom exceptions
  • Repository exception translation
  • Consistent error responses
  • Correlation IDs
  • Checked and unchecked exception decisions
  • Retryability classification
  • Partial-failure analysis
  • Final failure-handling design challenge