Final StretchOrdered learning track

Error Management Architecture

Learn Java Error, Reliability & Observability Engineering - Part 033

Error management architecture untuk sistem Java produksi: error catalog, boundary translation, observability mapping, audit evidence, governance, dan incident feedback loop.

14 min read2646 words
PrevNext
Lesson 3335 lesson track3035 Final Stretch
#java#reliability#error-handling#observability+3 more

Part 033 — Error Management Architecture

Di part sebelumnya kita sudah membahas exception semantics, error contract, retry, timeout, fallback, shutdown, logging, metrics, tracing, telemetry quality, alerting, dan debugging produksi. Sekarang kita naik satu level: bagaimana semua itu disusun menjadi architecture yang konsisten.

Di sistem kecil, error handling sering cukup dengan try/catch dan global exception handler. Di sistem production-grade, apalagi sistem case management, regulatory, payment, banking, enforcement, identity, atau workflow-heavy platform, pendekatan itu cepat runtuh. Error tidak hanya perlu “ditangani”; error perlu diklasifikasi, diterjemahkan, diobservasi, diaudit, dipakai untuk keputusan retry/degradation, dan dimasukkan kembali ke proses perbaikan sistem.

Tujuan part ini: membangun mental model dan blueprint Error Management Architecture untuk sistem Java modern.


1. Kaufman Skill Slice

Berdasarkan pendekatan Josh Kaufman, skill besar ini dipecah menjadi sub-skill kecil yang bisa dilatih secara deliberate.

Sub-skillTarget kemampuan
Failure classificationMampu membedakan domain rejection, validation error, dependency failure, platform failure, programmer defect, dan unknown outcome
Error contract designMampu membuat error code, Problem Details, retryability, severity, dan safe message yang stabil
Exception architectureMampu membuat hierarchy yang tidak meledak dan tidak terlalu generik
Boundary translationMampu menerjemahkan error internal ke HTTP, messaging, batch, CLI, audit, dan telemetry
Observability mappingMampu menghasilkan log, metric, trace, event, dan alert yang konsisten dari satu error model
GovernanceMampu menjaga error catalog tetap valid, tidak redundant, tidak bocor data sensitif, dan tidak menjadi sampah operasional
Feedback loopMampu menghubungkan incident, postmortem, runbook, telemetry, dan code change

Ukuran keberhasilan bukan “tidak ada exception”. Ukuran keberhasilan adalah:

  1. failure bisa diklasifikasi dengan cepat;
  2. caller tahu apakah bisa retry, fix input, escalate, atau stop;
  3. operator bisa menemukan evidence tanpa menebak;
  4. sistem tidak memperparah kondisi failure;
  5. domain/audit record tetap defensible;
  6. incident menghasilkan perbaikan desain, bukan hanya patch.

2. Masalah yang Diselesaikan Error Management Architecture

Tanpa arsitektur error, organisasi biasanya mengalami gejala berikut:

GejalaAkar masalah
Banyak RuntimeException("failed")Tidak ada failure taxonomy dan error catalog
Client melihat error message teknisBoundary translation lemah
Support tidak tahu arti error codeError code tidak punya registry dan owner
Alert noisyError tidak punya severity dan retryability yang konsisten
Retry memperparah outageError tidak dibedakan transient/permanent/unknown
Log tidak bisa dikorelasikan dengan traceContext propagation dan log schema tidak distandarkan
Incident sulit direkonstruksiError tidak menghasilkan evidence event dan audit trail
Exception hierarchy terlalu banyakClass digunakan untuk semua metadata, bukan error descriptor
Semua error jadi HTTP 500Domain, validation, conflict, policy, dan dependency error tidak diterjemahkan
Postmortem hanya menyalahkan orangTidak ada feedback loop ke invariant, test, telemetry, dan runbook

Arsitektur error yang baik membuat error menjadi first-class operational object, bukan efek samping dari stack trace.


3. Mental Model: Error sebagai Control Signal

Error bukan hanya “kejadian buruk”. Dalam sistem produksi, error adalah control signal yang mengarahkan keputusan berikut:

Kesalahan umum adalah memperlakukan error sebagai “output terakhir”. Padahal error justru memicu banyak downstream decision.

Contoh:

payment.dependency.timeout

Satu error code ini harus menjawab:

  • Apakah caller boleh retry?
  • Apakah operasi mungkin sudah berhasil tapi response hilang?
  • Apakah user boleh melihat pesan ini?
  • Apakah error ini masuk SLO?
  • Apakah alert harus firing?
  • Apakah event audit perlu dibuat?
  • Apakah fallback boleh dilakukan?
  • Apakah message harus masuk DLQ?
  • Apakah support bisa meminta user mencoba ulang?

Kalau error hanya berupa TimeoutException, terlalu banyak keputusan penting menjadi implicit.


4. Layer Arsitektur

Error management architecture bisa dilihat sebagai beberapa layer.

Prinsipnya: domain boleh mendefinisikan failure meaning, tetapi boundary yang menentukan bentuk response.

Domain tidak seharusnya tahu HTTP status. Persistence layer tidak seharusnya tahu Problem Details. Global exception handler tidak seharusnya mengarang error code sendiri.


5. Core Components

5.1 Error Catalog

Error catalog adalah registry resmi untuk error yang dapat dikenali sistem.

Contoh minimal field:

FieldMakna
codeStable machine-readable code
categoryDomain, validation, dependency, platform, security, unknown
severityInfo, warning, error, critical
retryableApakah retry aman secara prinsip
httpStatusMapping default untuk HTTP boundary
safeMessagePesan aman untuk client/operator
ownerTim/service owner
sloImpactApakah error ini memengaruhi SLO tertentu
auditRequiredApakah harus menjadi audit evidence
runbookLink/identifier runbook
deprecatedStatus lifecycle error code

Contoh Java:

public enum ErrorCategory {
    VALIDATION,
    DOMAIN_REJECTION,
    STATE_CONFLICT,
    POLICY_DENIAL,
    DEPENDENCY_FAILURE,
    INFRASTRUCTURE_FAILURE,
    PLATFORM_FAILURE,
    PROGRAMMER_DEFECT,
    UNKNOWN
}

public enum ErrorSeverity {
    INFO,
    WARNING,
    ERROR,
    CRITICAL
}

public record ErrorDescriptor(
        String code,
        ErrorCategory category,
        ErrorSeverity severity,
        boolean retryable,
        boolean auditRequired,
        boolean userCorrectable,
        int defaultHttpStatus,
        String safeMessage,
        String owner,
        String runbookId
) {}

Contoh catalog:

public final class ErrorCatalog {
    public static final ErrorDescriptor CASE_STATE_CONFLICT = new ErrorDescriptor(
            "case.state.conflict",
            ErrorCategory.STATE_CONFLICT,
            ErrorSeverity.WARNING,
            false,
            true,
            false,
            409,
            "The case is not in a state that allows this operation.",
            "case-platform",
            "RB-CASE-409"
    );

    public static final ErrorDescriptor PAYMENT_TIMEOUT = new ErrorDescriptor(
            "payment.dependency.timeout",
            ErrorCategory.DEPENDENCY_FAILURE,
            ErrorSeverity.ERROR,
            true,
            true,
            false,
            504,
            "The payment provider did not respond in time.",
            "payment-integration",
            "RB-PAYMENT-TIMEOUT"
    );

    private ErrorCatalog() {}
}

Error catalog tidak harus selalu enum. Untuk organisasi besar, catalog sering lebih tepat sebagai YAML/JSON/DB-backed registry yang di-generate menjadi Java constants, dokumentasi API, dashboard labels, dan runbook index.


5.2 Application Exception

Exception hierarchy sebaiknya membawa ErrorDescriptor dan metadata aman, bukan seluruh detail internal.

public abstract class ApplicationException extends RuntimeException {
    private final ErrorDescriptor descriptor;
    private final Map<String, String> safeAttributes;

    protected ApplicationException(
            ErrorDescriptor descriptor,
            String internalMessage,
            Throwable cause,
            Map<String, String> safeAttributes
    ) {
        super(internalMessage, cause);
        this.descriptor = Objects.requireNonNull(descriptor);
        this.safeAttributes = Map.copyOf(safeAttributes);
    }

    public ErrorDescriptor descriptor() {
        return descriptor;
    }

    public Map<String, String> safeAttributes() {
        return safeAttributes;
    }
}

Subclass bisa tetap sedikit:

public final class DomainRejectionException extends ApplicationException {
    public DomainRejectionException(
            ErrorDescriptor descriptor,
            String internalMessage,
            Map<String, String> safeAttributes
    ) {
        super(descriptor, internalMessage, null, safeAttributes);
    }
}

public final class DependencyFailureException extends ApplicationException {
    public DependencyFailureException(
            ErrorDescriptor descriptor,
            String internalMessage,
            Throwable cause,
            Map<String, String> safeAttributes
    ) {
        super(descriptor, internalMessage, cause, safeAttributes);
    }
}

Prinsip penting:

  • Class menjelaskan jenis mekanisme failure.
  • Descriptor menjelaskan kontrak operasional failure.
  • Safe attributes menjelaskan context yang boleh keluar.
  • Cause chain menyimpan evidence teknis internal.

Jangan membuat satu class untuk setiap error code kecuali domain benar-benar membutuhkannya sebagai type-level distinction.


5.3 Error Classification Service

Tidak semua exception lahir sebagai ApplicationException. Banyak failure berasal dari library/framework: JDBC, HTTP client, broker client, JSON parser, validation framework, crypto provider, filesystem, JVM, dan sebagainya.

Maka perlu classifier:

public final class ErrorClassifier {

    public ClassifiedError classify(Throwable throwable) {
        if (throwable instanceof ApplicationException app) {
            return ClassifiedError.from(app.descriptor(), app.safeAttributes(), app);
        }

        if (isTimeout(throwable)) {
            return ClassifiedError.from(
                    ErrorCatalog.PAYMENT_TIMEOUT,
                    Map.of("dependency", "payment-provider"),
                    throwable
            );
        }

        if (isOptimisticLockConflict(throwable)) {
            return ClassifiedError.from(
                    ErrorCatalog.CASE_STATE_CONFLICT,
                    Map.of(),
                    throwable
            );
        }

        return ClassifiedError.from(
                SystemErrors.UNKNOWN_INTERNAL_ERROR,
                Map.of(),
                throwable
        );
    }

    private boolean isTimeout(Throwable throwable) {
        return findCause(throwable, java.net.SocketTimeoutException.class).isPresent()
                || findCause(throwable, java.util.concurrent.TimeoutException.class).isPresent();
    }

    private boolean isOptimisticLockConflict(Throwable throwable) {
        return throwable.getClass().getName().contains("OptimisticLock");
    }

    private static <T extends Throwable> Optional<T> findCause(Throwable throwable, Class<T> type) {
        Throwable current = throwable;
        while (current != null) {
            if (type.isInstance(current)) {
                return Optional.of(type.cast(current));
            }
            current = current.getCause();
        }
        return Optional.empty();
    }
}

Classifier harus deterministic. Jangan membuat mapping berdasarkan fragile string message kecuali tidak ada pilihan lain dan sudah dilindungi test.


5.4 Boundary Translator

Setelah error diklasifikasi, bentuk response tergantung boundary.

HTTP boundary

public ProblemDetail toProblemDetail(ClassifiedError error, URI instance) {
    ErrorDescriptor d = error.descriptor();

    ProblemDetail problem = ProblemDetail.forStatus(d.defaultHttpStatus());
    problem.setTitle(d.safeMessage());
    problem.setType(URI.create("https://errors.example.com/" + d.code()));
    problem.setInstance(instance);
    problem.setProperty("code", d.code());
    problem.setProperty("retryable", d.retryable());
    problem.setProperty("category", d.category().name());

    error.safeAttributes().forEach(problem::setProperty);
    return problem;
}

HTTP boundary harus menghindari:

  • raw exception message untuk client;
  • stack trace di response;
  • vendor-specific DB/HTTP/broker details;
  • error code yang berubah karena refactor internal;
  • penggunaan status 500 untuk semua hal.

Messaging boundary

public MessageFailureDecision decide(ClassifiedError error) {
    ErrorDescriptor d = error.descriptor();

    return switch (d.category()) {
        case VALIDATION, DOMAIN_REJECTION, POLICY_DENIAL -> MessageFailureDecision.rejectToDlq(d.code());
        case DEPENDENCY_FAILURE, INFRASTRUCTURE_FAILURE ->
                d.retryable()
                        ? MessageFailureDecision.retryLater(d.code())
                        : MessageFailureDecision.rejectToDlq(d.code());
        case PROGRAMMER_DEFECT, PLATFORM_FAILURE, UNKNOWN ->
                MessageFailureDecision.stopConsumerAndPage(d.code());
        default -> MessageFailureDecision.rejectToDlq(d.code());
    };
}

Messaging boundary harus menjawab:

  • ack?
  • nack?
  • retry?
  • delay?
  • DLQ?
  • poison message?
  • stop consumer?
  • create audit event?

Batch/job boundary

Batch tidak boleh hanya “failed”. Batch perlu outcome detail:

public record BatchFailure(
        String itemId,
        String errorCode,
        boolean retryable,
        String safeMessage,
        Map<String, String> attributes
) {}

public record BatchRunResult(
        String runId,
        int total,
        int succeeded,
        int failed,
        List<BatchFailure> failures
) {}

Batch boundary penting karena partial failure adalah normal, bukan exception khusus.


5.5 Observability Mapper

Satu classified error harus menghasilkan telemetry yang konsisten.

Contoh log:

log.atWarn()
        .setMessage("Case transition rejected")
        .addKeyValue("error.code", error.descriptor().code())
        .addKeyValue("error.category", error.descriptor().category())
        .addKeyValue("case.id", caseId)
        .addKeyValue("from.state", fromState)
        .addKeyValue("requested.event", event)
        .log();

Contoh metric:

Counter.builder("application_errors_total")
        .tag("error_code", error.descriptor().code())
        .tag("category", error.descriptor().category().name())
        .tag("retryable", Boolean.toString(error.descriptor().retryable()))
        .register(meterRegistry)
        .increment();

Contoh tracing:

Span span = Span.current();
span.setAttribute("error.code", error.descriptor().code());
span.setAttribute("error.category", error.descriptor().category().name());
span.setAttribute("error.retryable", error.descriptor().retryable());
span.recordException(error.throwable());
span.setStatus(StatusCode.ERROR, error.descriptor().safeMessage());

Catatan penting: error_code biasanya boleh menjadi metric tag jika registry-nya bounded. Jangan menjadikan message, userId, caseId, requestId, stackTraceHash sebagai high-cardinality metric tag.


5.6 Audit Evidence Writer

Untuk sistem regulatory, audit evidence bukan log biasa. Log dapat di-retain pendek, disampling, dipindah, atau tidak dirancang sebagai record hukum. Audit evidence harus punya schema, lifecycle, integrity, retention, dan access policy sendiri.

Contoh audit event:

public record ErrorAuditEvent(
        String eventId,
        Instant occurredAt,
        String actorId,
        String tenantId,
        String aggregateType,
        String aggregateId,
        String operation,
        String errorCode,
        String decision,
        Map<String, String> safeFacts,
        String traceId
) {}

Kapan audit event dibuat?

FailureAudit?Alasan
Validation typo pada field opsionalTidak selaluBisa terlalu noisy
Business rejection pada state transitionYaMenjelaskan kenapa aksi ditolak
Policy denialYaSecurity/compliance decision
Payment timeout unknown outcomeYaPerlu rekonsiliasi
Internal NullPointerExceptionTergantungBiasanya incident evidence, bukan domain audit
Manual override gagalYaHigh accountability action

Audit evidence harus menjawab “apa yang diketahui sistem saat itu”, bukan “apa yang kita simpulkan belakangan”.


6. Error Lifecycle

Error code perlu lifecycle. Tanpa lifecycle, error catalog berubah menjadi tempat sampah.

Proposed

Error baru diajukan saat ada failure meaning baru yang tidak bisa diwakili existing code.

Pertanyaan review:

  • Apakah ini benar-benar error baru atau variasi attribute?
  • Siapa owner-nya?
  • Boundary mana yang terkena?
  • Apakah safe message sudah aman?
  • Apakah retryable?
  • Apakah audit required?
  • Apakah masuk SLO?

Approved

Descriptor sudah disetujui dan bisa dipakai di code.

Active

Error muncul di production dan dipantau.

Deprecated

Error tidak boleh dipakai untuk flow baru, tetapi masih dikenali untuk backward compatibility.

Removed

Error tidak lagi diproduksi dan tidak lagi menjadi contract aktif. Untuk public API, removal biasanya butuh major version atau periode sunset.


7. Reference Architecture di Spring Boot

Berikut blueprint sederhana.

Controller advice

@RestControllerAdvice
public class ApiErrorHandler {
    private final ErrorClassifier classifier;
    private final HttpErrorTranslator translator;
    private final ErrorObservability observability;
    private final ErrorAuditWriter auditWriter;

    @ExceptionHandler(Throwable.class)
    public ResponseEntity<ProblemDetail> handle(Throwable throwable, HttpServletRequest request) {
        ClassifiedError error = classifier.classify(throwable);

        observability.record(error);
        auditWriter.recordIfRequired(error);

        ProblemDetail problem = translator.toProblemDetail(
                error,
                URI.create(request.getRequestURI())
        );

        return ResponseEntity
                .status(error.descriptor().defaultHttpStatus())
                .body(problem);
    }
}

Caveat: @ExceptionHandler(Throwable.class) harus menjadi last resort. Handler yang lebih spesifik tetap bisa ada untuk validation framework, authentication framework, dan binding errors, tetapi semuanya harus berakhir pada ClassifiedError.


8. Error Management untuk Domain Workflow

Untuk workflow/case management, error management harus selaras dengan state machine.

Contoh state transition:

Error domain tidak boleh hanya berkata “invalid operation”. Ia harus menjelaskan invariant yang gagal.

public final class CaseTransitionGuard {
    public void requireTransitionAllowed(CaseStatus from, CaseEvent event) {
        boolean allowed = switch (from) {
            case DRAFT -> event == CaseEvent.SUBMIT;
            case SUBMITTED -> event == CaseEvent.ASSIGN;
            case UNDER_REVIEW -> event == CaseEvent.APPROVE || event == CaseEvent.REJECT;
            case APPROVED, REJECTED -> event == CaseEvent.CLOSE;
            case CLOSED -> false;
        };

        if (!allowed) {
            throw new DomainRejectionException(
                    ErrorCatalog.CASE_STATE_CONFLICT,
                    "Transition not allowed: from=%s event=%s".formatted(from, event),
                    Map.of(
                            "fromState", from.name(),
                            "event", event.name()
                    )
            );
        }
    }
}

Manfaat architecture:

  • API mendapat 409 dengan error code stabil;
  • log punya fromState dan event;
  • audit event mencatat rejection;
  • metric application_errors_total{error_code="case.state.conflict"} naik;
  • trace menunjukkan span state transition gagal;
  • runbook/support tahu ini bukan outage dependency.

9. Cross-Service Error Contract

Dalam microservices, error dari service A sering menjadi dependency failure di service B. Jangan bocorkan internal code sembarangan.

Misalnya case.state.conflict dari Case Service dipanggil oleh Orchestration Service.

Di Orchestration Service:

Upstream responseLocal classification
400 validationCaller/input contract failure
401/403Policy/auth propagation failure
404Missing referenced entity atau stale command
409 domain conflictBusiness conflict atau state race
429Dependency throttling
5xxDependency failure
timeoutUnknown outcome if side effect possible

Error code upstream bisa disimpan sebagai attribute:

throw new DependencyFailureException(
        ErrorCatalog.CASE_SERVICE_CONFLICT,
        "Case service rejected transition",
        cause,
        Map.of(
                "dependency", "case-service",
                "upstream.error_code", upstreamCode,
                "case.id", caseId
        )
);

Jangan otomatis menjadikan upstream error code sebagai public error code service kita. Service boundary tetap punya ownership contract sendiri.


10. Error Policy Matrix

Architecture harus punya policy matrix yang disepakati.

CategoryHTTPMessagingRetryAuditAlert
Validation400/422DLQ/rejectNoSometimesNo
Domain rejection409/422DLQ/rejectNoOftenNo
Policy denial403DLQ/rejectNoYesSecurity-dependent
Dependency timeout504Retry/delayYes if idempotentSometimesIf SLO impact
Dependency 429429/503Retry with backoffYesNoIf sustained
DB unavailable503Retry/stopYesNoYes
Serialization poison message400/DLQDLQNoSometimesYes if spike
Programmer defect500Stop/DLQNoIncident evidenceYes
Unknown500Stop/DLQConservativeMaybeYes

Policy matrix mencegah setiap engineer membuat keputusan sendiri di setiap handler.


11. Governance: Menjaga Error Catalog Tetap Bersih

Error catalog yang buruk bisa sama berbahayanya dengan tidak punya catalog.

11.1 Naming convention

Gunakan format stabil:

<domain>.<capability>.<failure>

Contoh:

case.transition.not_allowed
case.assignment.assignee_unavailable
payment.provider.timeout
identity.token.expired
document.rendering.template_missing

Hindari:

ERROR_001
BAD_REQUEST
PAYMENT_FAILED
INTERNAL_ERROR_NEW
DB_ERROR_2

11.2 Jangan encode detail dinamis ke code

Buruk:

case.transition.from_draft_to_approved_not_allowed

Lebih baik:

case.transition.not_allowed

Dengan attributes:

{
  "fromState": "DRAFT",
  "event": "APPROVE"
}

11.3 Review checklist untuk error baru

Sebelum menambahkan error code baru:

  • Apakah existing code cukup?
  • Apakah failure meaning berbeda atau hanya context berbeda?
  • Apakah message aman untuk external client?
  • Apakah category benar?
  • Apakah retryable benar?
  • Apakah status HTTP benar?
  • Apakah audit required?
  • Apakah owner jelas?
  • Apakah runbook diperlukan?
  • Apakah metric cardinality tetap bounded?
  • Apakah ada test translator?

12. Testing Strategy

Error management architecture harus dites seperti business logic.

12.1 Unit test classifier

@Test
void classifiesTimeoutAsDependencyTimeout() {
    Throwable throwable = new RuntimeException(new SocketTimeoutException("read timed out"));

    ClassifiedError error = classifier.classify(throwable);

    assertThat(error.descriptor().code()).isEqualTo("payment.dependency.timeout");
    assertThat(error.descriptor().retryable()).isTrue();
}

12.2 Contract test HTTP error

@Test
void returnsProblemDetailForStateConflict() throws Exception {
    mockMvc.perform(post("/cases/C-123/approve"))
            .andExpect(status().isConflict())
            .andExpect(jsonPath("$.code").value("case.state.conflict"))
            .andExpect(jsonPath("$.retryable").value(false))
            .andExpect(jsonPath("$.title").exists())
            .andExpect(jsonPath("$.stackTrace").doesNotExist());
}

12.3 Telemetry test

Gunakan fake registry/exporter untuk memastikan error menghasilkan metric dan span attribute yang benar.

@Test
void recordsErrorMetricWithBoundedTags() {
    errorObservability.record(classifiedError);

    Counter counter = registry.find("application_errors_total")
            .tag("error_code", "case.state.conflict")
            .counter();

    assertThat(counter.count()).isEqualTo(1.0);
}

12.4 Catalog validation test

@Test
void allErrorDescriptorsHaveRequiredFields() {
    for (ErrorDescriptor descriptor : ErrorCatalogRegistry.all()) {
        assertThat(descriptor.code()).matches("[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+");
        assertThat(descriptor.safeMessage()).isNotBlank();
        assertThat(descriptor.owner()).isNotBlank();
        assertThat(descriptor.defaultHttpStatus()).isBetween(400, 599);
    }
}

13. Migration Strategy dari Sistem Legacy

Jangan migrasi error handling dengan big bang. Gunakan strangler approach.

Step 1 — Inventory

Kumpulkan:

  • global exception handler;
  • custom exceptions;
  • repeated error strings;
  • HTTP status mapping;
  • DLQ reason;
  • top production errors;
  • support tickets;
  • alert names;
  • runbooks.

Step 2 — Buat minimum catalog

Mulai dari 20–50 error paling penting, bukan seluruh kemungkinan.

Step 3 — Pasang classifier

Mapping legacy exception ke ClassifiedError tanpa mengubah semua domain code.

Step 4 — Pasang boundary translator

Mulai dari HTTP dan messaging boundary.

Step 5 — Tambahkan observability mapper

Pastikan error.code muncul di logs, metrics, dan traces.

Step 6 — Refactor domain secara bertahap

Ganti throw generik dengan domain/application exception.

Step 7 — Governance

Tambahkan review, test, owner, lifecycle.


14. Production Checklist

Gunakan checklist ini saat review service Java.

Error catalog

  • Semua public error punya stable code.
  • Error code punya owner.
  • Error code punya category.
  • Error code punya retryability.
  • Error code punya safe message.
  • Error code punya HTTP/default boundary mapping.
  • Error code high-impact punya runbook.

Exception design

  • Cause chain tidak hilang.
  • Stack trace tidak dibuang tanpa alasan.
  • Exception hierarchy tidak terlalu generik.
  • Exception hierarchy tidak terlalu granular.
  • Domain rejection tidak dicampur dengan infrastructure failure.

Boundary translation

  • HTTP response tidak membocorkan internal message.
  • Messaging boundary punya ack/nack/DLQ policy.
  • Batch boundary mendukung partial failure.
  • Unknown outcome ditandai eksplisit.

Observability

  • Log punya error.code.
  • Metric punya bounded labels.
  • Trace merekam exception dan status.
  • Audit event dibuat untuk decision penting.
  • Alert mengarah ke symptom/SLO, bukan semua exception.

Governance

  • Error catalog diuji otomatis.
  • Error baru direview.
  • Deprecated code punya migration plan.
  • Postmortem bisa menghasilkan perubahan catalog/runbook/telemetry.

15. Anti-Decision: Hal yang Tidak Perlu Diarsitekturkan Berlebihan

Tidak semua aplikasi butuh full error management plane. Hindari overengineering jika:

  • service internal kecil tanpa external contract;
  • failure tidak punya dampak audit/compliance;
  • tim sangat kecil dan masih eksploratif;
  • error code belum dipakai oleh client/ops/support;
  • deployment masih single service sederhana.

Namun tetap perlu minimal:

  • preserve cause;
  • structured logs;
  • safe client message;
  • correct status;
  • timeout/retry policy;
  • bounded metrics;
  • clear ownership.

Architecture harus tumbuh mengikuti risk, bukan mengikuti template.


16. Deliberate Practice

Latihan 1 — Bangun error catalog mini

Ambil satu service yang kamu kenal. Buat 15 error descriptor:

  • 5 domain/validation;
  • 5 dependency/infrastructure;
  • 3 security/policy;
  • 2 unknown/platform.

Untuk tiap descriptor, isi:

  • code;
  • category;
  • retryable;
  • HTTP status;
  • safe message;
  • owner;
  • audit required;
  • runbook id.

Latihan 2 — Refactor global exception handler

Ubah global exception handler dari langsung mapping exception ke response menjadi:

Throwable -> ClassifiedError -> ProblemDetail + telemetry + audit

Latihan 3 — Review metric cardinality

Pastikan semua metric error hanya memakai bounded tags.

Buruk:

error_message="case C-991 transition from DRAFT to APPROVE failed for user U-123"

Baik:

error_code="case.transition.not_allowed"
category="state_conflict"

Latihan 4 — Buat incident reconstruction

Ambil satu error code. Pastikan dari error code itu kamu bisa menemukan:

  • log event;
  • trace span;
  • metric graph;
  • alert;
  • runbook;
  • audit evidence;
  • owner.

Jika tidak bisa, architecture belum lengkap.


17. Ringkasan

Error management architecture adalah cara menjadikan failure sebagai objek operasional yang konsisten.

Intinya:

  • error harus diklasifikasi;
  • classification harus menghasilkan decision;
  • decision harus diterjemahkan sesuai boundary;
  • telemetry harus konsisten;
  • audit evidence harus terpisah dari log biasa;
  • error code harus punya lifecycle dan owner;
  • incident harus memperbaiki catalog, test, telemetry, dan runbook.

Arsitektur ini bukan dekorasi. Ia adalah mekanisme untuk mengurangi waktu diagnosis, mencegah cascading failure, menjaga kontrak API, mendukung support, dan membuat keputusan sistem defensible.


18. Referensi Primer

  • Java SE 25 API — Throwable sebagai superclass semua error dan exception di Java.
  • Java Language Specification Java SE 25 — exception semantics dan checked/unchecked exception.
  • RFC 9457 — Problem Details for HTTP APIs.
  • OpenTelemetry Documentation — observability framework untuk logs, metrics, dan traces.
  • Google SRE Workbook — postmortem culture dan incident learning loop.
Lesson Recap

You just completed lesson 33 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.