Fallback & Graceful Degradation
Learn Java Error, Reliability & Observability Engineering - Part 015
Fallback and graceful degradation sebagai strategi reliability yang eksplisit, aman, terukur, dan dapat dipertanggungjawabkan di sistem Java produksi.
Part 015 — Fallback & Graceful Degradation
1. Inti Pembelajaran
Fallback dan graceful degradation sering dipahami terlalu dangkal sebagai:
“Kalau dependency gagal, kasih default value.”
Itu berbahaya.
Dalam sistem produksi, terutama sistem regulatori, enforcement lifecycle, case management, finance, identity, risk, policy, atau workflow kritikal, fallback bukan sekadar nilai pengganti. Fallback adalah mode operasi alternatif yang sengaja dirancang ketika sistem tidak dapat memenuhi jalur utama secara penuh.
Definisi kerja:
Fallback adalah keputusan eksplisit untuk mengembalikan hasil alternatif saat jalur utama gagal, dengan batasan domain, observability, dan konsekuensi bisnis yang jelas.
Graceful degradation adalah kemampuan sistem untuk mengurangi fungsi, kualitas, cakupan, kesegaran data, atau otomatisasi tanpa berubah menjadi salah, tidak aman, tidak dapat diaudit, atau tidak dapat dipulihkan.
Target part ini bukan membuat aplikasi “selalu sukses”. Targetnya adalah membuat aplikasi:
- tetap aman saat dependency gagal,
- jujur terhadap caller bahwa respons terdegradasi,
- tidak menyembunyikan kerusakan sistem,
- tidak menciptakan data palsu,
- tidak memperbesar blast radius,
- tetap observable,
- tetap bisa dipertanggungjawabkan setelah incident.
2. Kaufman Skill Deconstruction
Mengikuti pendekatan Josh Kaufman, skill “fallback & degradation” kita pecah menjadi sub-skill kecil yang bisa dilatih.
| Sub-skill | Kemampuan yang Harus Dikuasai | Output Praktis |
|---|---|---|
| Failure classification | Membedakan transient, persistent, partial, overload, stale, dan unknown failure | Failure policy matrix |
| Domain impact analysis | Menentukan apakah hasil fallback aman secara domain | Fallback decision record |
| Degradation design | Mendesain partial capability, cached response, read-only mode, queue-later mode | Degradation mode catalog |
| Error honesty | Menyampaikan degraded state ke caller tanpa membocorkan detail internal | API response contract |
| Observability | Mengukur kapan fallback terjadi, kenapa, berapa lama, dan siapa terdampak | Logs, metrics, traces |
| Recovery thinking | Merancang bagaimana sistem kembali ke normal mode | Recovery invariant |
| Testing | Membuktikan fallback tidak merusak invariant | Failure injection test |
Prinsip Kaufman di sini:
Jangan belajar fallback sebagai pattern. Belajar fallback sebagai decision discipline.
3. Mental Model: Fallback Adalah State, Bukan Catch Block
Anti-pattern umum:
try {
return recommendationClient.getRecommendations(userId);
} catch (Exception e) {
return List.of();
}
Secara sintaksis ini terlihat aman. Secara sistemik bisa berbahaya.
Pertanyaan yang tidak terjawab:
- Apakah empty list berarti benar-benar tidak ada rekomendasi?
- Apakah caller tahu dependency gagal?
- Apakah metric mencatat fallback?
- Apakah user experience berbeda?
- Apakah retry akan terjadi di layer atas?
- Apakah keputusan bisnis berubah karena data kosong palsu?
- Apakah tim operasi tahu service downstream sedang rusak?
Fallback yang benar harus diperlakukan sebagai state eksplisit:
Jika fallback tidak punya state, maka fallback hanya “exception swallowing with better branding”.
4. Perbedaan Fallback, Retry, Timeout, Circuit Breaker, dan Degradation
Part sebelumnya membahas retry, timeout, circuit breaker, bulkhead, dan rate limit. Fallback tidak menggantikan semua itu.
| Mekanisme | Pertanyaan yang Dijawab | Contoh |
|---|---|---|
| Timeout | “Berapa lama kita mau menunggu?” | Stop call setelah 500ms |
| Retry | “Apakah operasi aman diulang?” | Retry 2x dengan jitter |
| Circuit breaker | “Apakah dependency harus dihentikan sementara?” | Open circuit setelah error rate tinggi |
| Bulkhead | “Bagaimana membatasi kerusakan?” | Pool terpisah untuk dependency lambat |
| Rate limiter | “Apakah traffic boleh masuk?” | Batasi request per tenant |
| Fallback | “Apa respons aman jika jalur utama tidak tersedia?” | Return stale cached profile dengan flag degraded |
| Degradation | “Kapabilitas mana yang dikurangi?” | Read-only mode, manual review, partial response |
Urutan desain yang sehat:
5. Prinsip Utama Fallback Produksi
5.1 Fallback Harus Aman, Bukan Sekadar Tersedia
Sistem yang selalu memberi respons belum tentu reliable.
Contoh berbahaya:
public RiskDecision assessRisk(Transaction tx) {
try {
return riskEngine.assess(tx);
} catch (Exception e) {
return RiskDecision.APPROVE;
}
}
Ini meningkatkan availability palsu, tetapi menghancurkan risk invariant.
Fallback yang lebih aman:
public RiskDecision assessRisk(Transaction tx) {
try {
return riskEngine.assess(tx);
} catch (RiskEngineUnavailableException e) {
return RiskDecision.manualReview(
tx.id(),
"RISK_ENGINE_UNAVAILABLE",
"Risk decision deferred to manual review"
);
}
}
Perbedaannya:
- sistem tidak pura-pura berhasil,
- risiko tidak otomatis disetujui,
- ada alasan eksplisit,
- ada jalur operasional berikutnya,
- hasil tetap bisa diaudit.
5.2 Fallback Harus Dibedakan dari Normal Result
Jika caller tidak bisa membedakan normal dan fallback, maka fallback mencemari domain semantics.
Buruk:
record CustomerProfile(String id, String name, String segment) {}
Lebih baik:
record CustomerProfileResponse(
CustomerProfile profile,
ResponseMode mode,
String degradationReason,
Instant dataFreshness
) {}
enum ResponseMode {
NORMAL,
DEGRADED_CACHE,
DEGRADED_PARTIAL,
DEGRADED_MANUAL_REVIEW,
FAILED_CLOSED
}
Dengan ini, caller bisa membuat keputusan:
- apakah boleh menampilkan data,
- apakah boleh melakukan action,
- apakah perlu banner “data may be stale”,
- apakah perlu audit event,
- apakah perlu retry later.
5.3 Fallback Harus Bounded
Fallback yang tidak dibatasi bisa menjadi silent outage.
Contoh batasan:
| Batasan | Contoh |
|---|---|
| Waktu | Cache fallback hanya boleh 15 menit |
| Scope | Hanya untuk read API, bukan write API |
| Tenant | Hanya tenant low-risk atau sandbox |
| Operation | Hanya inquiry, bukan approval |
| Volume | Maksimum 5% request boleh fallback |
| Business state | Tidak boleh fallback untuk case dalam escalation |
| Data class | Tidak boleh fallback untuk PII sensitif |
Invariant:
Semakin besar konsekuensi bisnis, semakin sempit ruang fallback.
5.4 Fallback Harus Observable
Fallback yang tidak terlihat adalah outage yang tertunda.
Minimum telemetry:
| Signal | Data yang Dicatat |
|---|---|
| Log | reason, dependency, operation, correlationId, tenantId, fallbackMode |
| Metric | fallback count/rate by operation and reason |
| Trace | span event fallback.selected |
| Alert | fallback rate melewati threshold |
| Audit | untuk domain-regulated decision |
Contoh structured log:
logger.warn("fallback_selected operation={} mode={} reason={} dependency={} correlationId={}",
operation,
fallbackMode,
reasonCode,
dependencyName,
correlationId);
6. Taxonomy Fallback
6.1 Default Value Fallback
Mengembalikan nilai default.
Contoh:
return UserPreferences.defaultPreferences(userId);
Cocok untuk:
- preference UI,
- non-critical personalization,
- optional metadata,
- low-risk enrichment.
Tidak cocok untuk:
- risk score,
- authorization,
- payment limit,
- regulatory status,
- ownership status,
- fraud decision.
Rule:
Default value hanya aman jika default tersebut merupakan keputusan domain yang valid, bukan tebakan teknis.
6.2 Cached/Stale Data Fallback
Menggunakan data lama saat sumber utama tidak tersedia.
public CustomerProfileResponse getProfile(CustomerId customerId) {
try {
CustomerProfile fresh = profileClient.fetch(customerId);
cache.put(customerId, fresh);
return CustomerProfileResponse.normal(fresh);
} catch (ProfileServiceUnavailableException e) {
return cache.find(customerId)
.filter(cached -> cached.age().compareTo(Duration.ofMinutes(15)) <= 0)
.map(CustomerProfileResponse::degradedCache)
.orElseThrow(() -> new ProfileUnavailableException(customerId, e));
}
}
Pertanyaan desain:
- Seberapa stale data yang masih aman?
- Apakah caller harus tahu data stale?
- Apakah data stale boleh dipakai untuk decision atau hanya display?
- Apakah cache harus invalidated setelah event tertentu?
- Apakah cache mengandung PII?
- Apakah cache konsisten lintas tenant?
6.3 Partial Response Fallback
Mengembalikan sebagian data ketika dependency enrichment gagal.
record CaseOverview(
String caseId,
String status,
Optional<RiskSummary> riskSummary,
Optional<AssignmentInfo> assignment,
ResponseMode mode
) {}
Cocok untuk:
- dashboard,
- search result,
- non-critical enrichment,
- read-only screen.
Risiko:
- UI salah menginterpretasikan field kosong,
- user mengambil keputusan dari data tidak lengkap,
- field optional dipakai sebagai domain signal,
- missing data dianggap “tidak ada masalah”.
Rule:
Partial response harus menandai field yang unavailable, bukan menyamarkannya sebagai empty atau null normal.
6.4 Read-Only Mode
Sistem menolak mutasi tetapi tetap melayani baca.
Cocok ketika:
- database primary write bermasalah,
- downstream decision service tidak tersedia,
- regulatory audit writer gagal,
- event bus tidak bisa menjamin publish,
- consistency invariant tidak bisa dijaga.
Contoh policy:
public void updateCaseStatus(UpdateCaseStatusCommand command) {
if (systemMode.isReadOnly()) {
throw new SystemDegradedException(
"CASE_WRITE_DISABLED",
"Case status update is temporarily disabled because audit writer is unavailable"
);
}
caseWorkflow.updateStatus(command);
}
Read-only mode sering lebih baik daripada “best effort write” yang kehilangan audit trail.
6.5 Queue-for-Later Fallback
Jika operasi tidak bisa diproses sekarang, simpan intent untuk diproses nanti.
Cocok untuk:
- notification,
- non-urgent synchronization,
- reconciliation job,
- external reporting,
- asynchronous enrichment.
Tidak cocok jika:
- caller butuh immediate decision,
- operation tidak idempotent,
- order sangat kritikal dan tidak terjamin,
- delayed processing mengubah outcome hukum/bisnis,
- tidak ada reconciliation process.
Pattern:
6.6 Manual Review Fallback
Untuk domain high-risk, fallback terbaik sering bukan default, melainkan human/manual decision.
Contoh:
public EnforcementDecision decide(CaseFile file) {
try {
return decisionEngine.decide(file);
} catch (DecisionEngineUnavailableException e) {
manualReviewQueue.enqueue(file.id(), "DECISION_ENGINE_UNAVAILABLE");
return EnforcementDecision.pendingManualReview(file.id());
}
}
Cocok untuk:
- enforcement action,
- fraud decision,
- account restriction,
- licensing approval,
- sanctions/policy-sensitive workflow.
Kelemahan:
- operational backlog,
- human inconsistency,
- SLA impact,
- escalation burden.
Tetapi secara defensibility, ini sering jauh lebih baik daripada keputusan otomatis yang tidak punya data cukup.
6.7 Feature Disablement
Menonaktifkan fitur non-critical untuk menjaga core journey tetap berjalan.
Contoh:
- disable recommendation,
- disable analytics enrichment,
- disable preview generation,
- disable export besar,
- disable auto-assignment,
- disable background indexing.
Prinsip:
Degrade optional capability first. Protect core invariant last.
7. Fail Open, Fail Closed, Fail Soft, Fail Hard
7.1 Fail Closed
Fail closed berarti sistem menolak operasi saat tidak bisa memastikan keamanan/kebenaran.
Contoh:
if (!authorizationService.canVerify(user, action)) {
throw new AccessDecisionUnavailableException("AUTHZ_DECISION_UNAVAILABLE");
}
Cocok untuk:
- authorization,
- identity proofing,
- payment approval,
- risk approval,
- enforcement escalation,
- legal/regulatory irreversible action.
7.2 Fail Open
Fail open berarti sistem mengizinkan operasi walau sebagian pemeriksaan gagal.
Cocok hanya jika:
- dampak risiko rendah,
- consequence reversible,
- decision bisa direkonsiliasi,
- user impact fail-closed lebih buruk,
- ada audit trail,
- policy menyetujui.
Contoh relatif aman:
public boolean shouldShowWelcomeBanner(UserId userId) {
try {
return personalizationClient.shouldShowBanner(userId);
} catch (Exception e) {
return false;
}
}
7.3 Fail Soft
Fail soft berarti sistem tetap berjalan dengan kapabilitas terbatas.
Contoh:
- dashboard tampil tanpa risk summary,
- export berjalan tanpa enrichment optional,
- search berjalan tanpa sorting personalization,
- create case diterima tetapi auto-assignment ditunda.
7.4 Fail Hard
Fail hard berarti operasi dihentikan eksplisit.
Contoh:
- audit write tidak tersedia untuk mutasi regulated,
- idempotency store tidak tersedia untuk payment-like command,
- authorization decision unavailable,
- state transition guard tidak bisa dievaluasi.
Decision table:
| Scenario | Recommended Mode | Alasan |
|---|---|---|
| Authorization service down | Fail closed | Tidak boleh memberi akses tanpa keputusan |
| Recommendation service down | Fail soft | Non-critical enrichment |
| Audit writer down for regulated mutation | Fail hard/read-only | Mutasi tanpa audit tidak defensible |
| Notification service down | Queue for later | Side effect bisa delayed |
| Risk engine down for approval | Manual review | Decision high-risk |
| Profile service down for display | Stale cache with marker | Read-only dan bounded |
| Idempotency store down | Fail hard | Duplicate side effect risk |
8. Java Design Pattern: Degraded Result
Buat degraded state sebagai tipe eksplisit.
public sealed interface ServiceResult<T>
permits ServiceResult.Success, ServiceResult.Degraded, ServiceResult.Failure {
record Success<T>(T value) implements ServiceResult<T> {}
record Degraded<T>(
T value,
DegradationMode mode,
String reasonCode,
Instant observedAt
) implements ServiceResult<T> {}
record Failure<T>(
String errorCode,
String message
) implements ServiceResult<T> {}
}
public enum DegradationMode {
STALE_CACHE,
PARTIAL_RESPONSE,
MANUAL_REVIEW,
READ_ONLY,
QUEUED_FOR_LATER,
FEATURE_DISABLED
}
Pemakaian:
public ServiceResult<CaseOverview> getCaseOverview(CaseId caseId) {
CaseSummary summary = caseRepository.findSummary(caseId)
.orElseThrow(() -> new CaseNotFoundException(caseId));
try {
RiskSummary risk = riskClient.getRiskSummary(caseId);
return new ServiceResult.Success<>(CaseOverview.withRisk(summary, risk));
} catch (RiskServiceUnavailableException e) {
return new ServiceResult.Degraded<>(
CaseOverview.withoutRisk(summary),
DegradationMode.PARTIAL_RESPONSE,
"RISK_SUMMARY_UNAVAILABLE",
Instant.now()
);
}
}
Boundary mapper:
public ResponseEntity<CaseOverviewResponse> toHttp(ServiceResult<CaseOverview> result) {
return switch (result) {
case ServiceResult.Success<CaseOverview> success ->
ResponseEntity.ok(CaseOverviewResponse.normal(success.value()));
case ServiceResult.Degraded<CaseOverview> degraded ->
ResponseEntity.ok()
.header("X-Response-Mode", "degraded")
.header("X-Degradation-Reason", degraded.reasonCode())
.body(CaseOverviewResponse.degraded(
degraded.value(),
degraded.mode(),
degraded.reasonCode()
));
case ServiceResult.Failure<CaseOverview> failure ->
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(CaseOverviewResponse.failed(failure.errorCode()));
};
}
Catatan:
- Tidak semua API harus expose header seperti ini.
- Untuk public API, gunakan kontrak formal seperti Problem Details atau response metadata.
- Untuk internal API, header dapat membantu debugging dan routing.
- Jangan expose dependency internal jika itu informasi sensitif.
9. Java Design Pattern: Fallback Policy Object
Jangan sebarkan keputusan fallback di banyak catch block.
Buruk:
try {
return client.call();
} catch (TimeoutException e) {
return cache.get();
} catch (IOException e) {
return cache.get();
} catch (RuntimeException e) {
return defaultValue();
}
Lebih baik:
public final class FallbackPolicy {
public FallbackDecision decide(FailureContext context) {
if (context.operation().isWrite()) {
return FallbackDecision.failClosed("WRITE_FALLBACK_NOT_ALLOWED");
}
if (context.failureType() == FailureType.AUTHORIZATION_UNAVAILABLE) {
return FallbackDecision.failClosed("AUTHZ_UNAVAILABLE_FAIL_CLOSED");
}
if (context.failureType() == FailureType.READ_DEPENDENCY_TIMEOUT
&& context.cacheAge().compareTo(Duration.ofMinutes(15)) <= 0) {
return FallbackDecision.useStaleCache("STALE_CACHE_ALLOWED");
}
if (context.operation().isNonCriticalEnrichment()) {
return FallbackDecision.partialResponse("OPTIONAL_ENRICHMENT_SKIPPED");
}
return FallbackDecision.failClosed("NO_SAFE_FALLBACK");
}
}
Supporting types:
public record FailureContext(
Operation operation,
FailureType failureType,
Duration cacheAge,
String dependency,
String tenantId,
String correlationId
) {}
public record FallbackDecision(
FallbackAction action,
String reasonCode
) {
static FallbackDecision failClosed(String reasonCode) {
return new FallbackDecision(FallbackAction.FAIL_CLOSED, reasonCode);
}
static FallbackDecision useStaleCache(String reasonCode) {
return new FallbackDecision(FallbackAction.USE_STALE_CACHE, reasonCode);
}
static FallbackDecision partialResponse(String reasonCode) {
return new FallbackDecision(FallbackAction.PARTIAL_RESPONSE, reasonCode);
}
}
enum FallbackAction {
FAIL_CLOSED,
USE_STALE_CACHE,
PARTIAL_RESPONSE,
QUEUE_FOR_LATER,
MANUAL_REVIEW
}
Manfaat:
- fallback rule bisa diuji terpisah,
- policy review lebih mudah,
- domain owner bisa memahami konsekuensi,
- audit lebih jelas,
- tidak ada fallback liar di catch block.
10. Fallback dan Domain Invariant
Sebelum mendesain fallback, tulis invariant.
Contoh case management:
| Invariant | Fallback yang Dilarang | Fallback yang Mungkin Aman |
|---|---|---|
| Case status transition harus valid | Update status walau rule engine down | Queue/manual review |
| Mutasi regulated harus punya audit event | Commit DB tanpa audit | Read-only/fail hard |
| Assignment tidak boleh ke officer tanpa permission | Default assign ke siapa saja | Pending assignment |
| Enforcement action butuh risk decision | Auto-approve | Manual review |
| User tidak boleh melihat restricted case | Fail open authz | Fail closed |
| UI dashboard boleh incomplete | Hard fail seluruh dashboard | Partial response |
Template invariant:
### Fallback Decision Record
Operation: <operation name>
Primary dependency: <dependency>
Failure modes: <timeout, 5xx, unavailable, stale, unknown>
Business consequence: <low/medium/high/critical>
Allowed fallback modes: <list>
Forbidden fallback modes: <list>
Maximum degradation duration: <duration>
Caller visibility: <explicit/hidden/internal>
Audit required: <yes/no>
Metrics required: <yes/no>
Recovery process: <manual/automatic/reconciliation>
Owner approval: <team/domain owner>
Top 1% engineer tidak hanya bertanya “bisa fallback apa?” Mereka bertanya:
“Fallback ini menjaga invariant apa, dan invariant apa yang dikorbankan?”
11. Fallback dan Data Freshness
Stale data bukan masalah teknis semata. Stale data adalah masalah domain.
Contoh:
| Data | Stale 5 menit | Stale 1 jam | Stale 1 hari |
|---|---|---|---|
| UI theme | Aman | Aman | Aman |
| Product catalog | Mungkin aman | Tergantung | Tergantung |
| Customer profile display | Mungkin aman | Perlu marker | Berisiko |
| Risk score | Berisiko | Tidak aman | Tidak aman |
| Authorization decision | Tidak aman | Tidak aman | Tidak aman |
| Regulatory case status | Tergantung state | Berisiko | Tidak aman |
Representasikan freshness:
public record CachedValue<T>(
T value,
Instant loadedAt,
Duration maxAge
) {
public boolean isFreshAt(Instant now) {
return Duration.between(loadedAt, now).compareTo(maxAge) <= 0;
}
public Duration ageAt(Instant now) {
return Duration.between(loadedAt, now);
}
}
Jangan hanya cache value. Cache juga metadata:
- loadedAt,
- source version,
- source system,
- tenant,
- invalidation reason,
- maximum allowed age,
- classification/sensitivity.
12. Fallback dan Consistency
Fallback bisa mengubah consistency model tanpa disadari.
Contoh:
- normal mode membaca data fresh dari primary service,
- fallback mode membaca cache lokal 10 menit,
- caller melakukan action berdasarkan data stale,
- action menyebabkan state transition yang tidak lagi valid.
Mitigasi:
- bedakan data untuk display vs decision,
- jangan gunakan fallback read untuk critical write decision,
- mark stale data explicitly,
- require revalidation before mutation,
- gunakan version/ETag/stateVersion,
- gunakan optimistic concurrency check.
Contoh:
public void approveCase(ApproveCaseCommand command) {
CaseSnapshot snapshot = caseRepository.get(command.caseId());
if (!snapshot.version().equals(command.expectedVersion())) {
throw new CaseConflictException(
command.caseId(),
"CASE_VERSION_CHANGED",
"Case changed after the user viewed it"
);
}
approvalPolicy.ensureCanApprove(snapshot);
caseRepository.approve(command.caseId());
}
Degraded read boleh terjadi, tetapi write harus revalidate.
13. Observability untuk Fallback
13.1 Logging
Log fallback sebagai event, bukan stack trace spam.
logger.warn(
"fallback_selected operation={} dependency={} mode={} reason={} tenant={} correlationId={}",
operation,
dependency,
mode,
reason,
tenantId,
correlationId
);
Jangan log seluruh exception setiap request jika downstream outage menghasilkan ribuan fallback per detik. Gunakan sampling, rate limiting, atau aggregate telemetry.
13.2 Metrics
Minimum metric:
fallback_total{operation,dependency,mode,reason}
fallback_ratio{operation,dependency}
fallback_duration_seconds{mode}
degraded_response_total{api,mode}
stale_cache_age_seconds{operation,dependency}
Cardinality warning:
Jangan masukkan caseId, userId, correlationId, atau raw error message sebagai metric tag.
13.3 Tracing
Span event:
Span.current().addEvent("fallback.selected", Attributes.of(
stringKey("app.operation"), operation,
stringKey("fallback.mode"), mode.name(),
stringKey("fallback.reason"), reason,
stringKey("dependency.name"), dependency
));
Jika menggunakan OpenTelemetry, fallback sebaiknya tampak sebagai event atau attribute di span yang relevan, bukan trace terpisah tanpa hubungan kausal.
13.4 Alerting
Alert yang baik:
- fallback ratio > baseline,
- fallback duration terlalu lama,
- fallback mode critical aktif,
- stale cache age mendekati batas,
- manual review backlog naik,
- read-only mode aktif untuk domain kritikal.
Alert yang buruk:
- alert setiap fallback tunggal,
- alert berdasarkan log message string,
- alert tanpa service owner,
- alert tanpa runbook,
- alert untuk fallback yang expected dan low-risk.
14. Fallback dan User Experience
Fallback yang baik tidak selalu harus terlihat ke end user, tetapi harus terlihat ke caller/operator.
| Scenario | User Visibility | Operator Visibility |
|---|---|---|
| Optional recommendation gagal | Bisa disembunyikan | Metric/log tetap ada |
| Dashboard partial | Tampilkan marker bagian unavailable | Metric/log/trace |
| Data stale | Tampilkan freshness jika relevan | Metric stale age |
| Write disabled | Tampilkan pesan eksplisit | Alert |
| Manual review fallback | Tampilkan status pending/manual review | Audit + queue metric |
Contoh API response:
{
"caseId": "CASE-123",
"status": "UNDER_REVIEW",
"riskSummary": null,
"meta": {
"responseMode": "DEGRADED_PARTIAL",
"degradationReason": "RISK_SUMMARY_UNAVAILABLE",
"safeForDecision": false
}
}
Field penting: safeForDecision.
Dalam sistem kompleks, response boleh cukup untuk display tetapi tidak cukup untuk decision.
15. Fallback dan Security
Fallback sering membuka celah security.
Anti-pattern:
try {
return permissionClient.canAccess(user, resource);
} catch (Exception e) {
return true;
}
Aturan keras:
- Authorization failure harus fail closed.
- Authentication uncertainty harus fail closed.
- Policy decision unavailable harus fail closed kecuali ada policy formal yang menyatakan sebaliknya.
- Fallback tidak boleh melewati audit/security hook.
- Fallback tidak boleh expose data sensitif karena masking service gagal.
- Fallback cache untuk data sensitif harus punya TTL dan encryption policy.
Security fallback yang sehat:
public AccessDecision canAccess(User user, Resource resource) {
try {
return policyDecisionPoint.evaluate(user, resource);
} catch (PolicyUnavailableException e) {
throw new AccessUnavailableException(
"ACCESS_DECISION_UNAVAILABLE",
"Access cannot be evaluated at this time",
e
);
}
}
16. Fallback dan Regulatory Defensibility
Dalam domain regulatori, pertanyaan setelah incident bukan hanya:
“Apakah sistem tetap online?”
Tetapi:
- keputusan apa yang dibuat saat informasi tidak lengkap?
- siapa/apa yang menyetujui fallback policy?
- apakah user/caller tahu bahwa response degraded?
- apakah mutasi tetap punya audit trail?
- apakah ada reconciliation?
- apakah ada bukti bahwa fallback bounded?
- apakah fallback pernah diuji?
- apakah data stale dipakai untuk keputusan yang tidak semestinya?
Untuk operasi regulated, buat fallback record:
public record FallbackAuditEvent(
String operation,
String entityId,
String fallbackMode,
String reasonCode,
Instant occurredAt,
String correlationId,
String actorId,
boolean decisionDeferred,
boolean mutationPerformed
) {}
Audit event tidak perlu untuk semua fallback low-risk, tetapi wajib untuk fallback yang mempengaruhi keputusan domain.
17. Testing Fallback
Fallback harus diuji sebagai first-class behavior.
17.1 Unit Test Policy
@Test
void shouldFailClosedWhenAuthorizationUnavailable() {
FallbackPolicy policy = new FallbackPolicy();
FallbackDecision decision = policy.decide(new FailureContext(
Operation.write("approve-case"),
FailureType.AUTHORIZATION_UNAVAILABLE,
Duration.ZERO,
"authz-service",
"tenant-a",
"corr-1"
));
assertThat(decision.action()).isEqualTo(FallbackAction.FAIL_CLOSED);
}
17.2 Integration Test Dependency Failure
@Test
void shouldReturnPartialResponseWhenRiskSummaryUnavailable() {
riskService.stubTimeout();
ResponseEntity<CaseOverviewResponse> response = restTemplate.getForEntity(
"/cases/CASE-123/overview",
CaseOverviewResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().meta().responseMode()).isEqualTo("DEGRADED_PARTIAL");
assertThat(response.getBody().meta().safeForDecision()).isFalse();
}
17.3 Chaos/Failure Injection Test
Simulasikan:
- timeout dependency,
- 5xx dependency,
- slow response,
- stale cache,
- missing cache,
- partial dependency failure,
- circuit open,
- audit writer unavailable,
- queue unavailable,
- recovery after outage.
17.4 Observability Test
Verifikasi:
- metric fallback bertambah,
- log structured berisi reason code,
- trace punya event fallback,
- alert threshold masuk akal,
- no high-cardinality metric tag,
- audit event tercatat jika required.
18. Common Anti-Patterns
18.1 Catch-All Fallback
catch (Exception e) {
return defaultValue;
}
Masalah:
- menyembunyikan bug,
- menyamakan semua failure,
- tidak observable,
- bisa mengubah semantics,
- sulit debug.
18.2 Fallback yang Tidak Pernah Diuji
Fallback code sering hanya jalan saat incident. Jika tidak diuji, fallback bisa lebih rusak daripada primary path.
18.3 Fallback dengan Dependency yang Sama
try {
return primaryClient.call();
} catch (Exception e) {
return fallbackClient.call(); // ternyata host/database/network sama
}
Fallback harus independen secara failure domain, atau setidaknya paham shared dependency.
18.4 Fallback yang Memicu Overload
Contoh:
- primary cache gagal,
- fallback hit database langsung,
- database overload,
- semua request makin lambat,
- retry meningkat,
- cascading failure.
18.5 Silent Degradation
Sistem degraded selama berhari-hari karena semua response 200 OK dan tidak ada alert.
18.6 Fallback Mengubah Authorization
Tidak boleh.
18.7 Empty Collection sebagai Error
return List.of();
Kadang empty berarti “tidak ada data”. Kadang berarti “gagal mengambil data”. Jangan campur.
19. Production Checklist
Sebelum menambahkan fallback, jawab:
- Apa primary operation?
- Failure apa yang ingin ditangani?
- Apakah operation read atau write?
- Apakah fallback aman secara domain?
- Apakah fallback mengubah consistency?
- Apakah fallback boleh memakai stale data?
- Berapa batas usia data?
- Apakah caller tahu response degraded?
- Apakah fallback memerlukan audit event?
- Apakah fallback punya metric?
- Apakah fallback punya trace event?
- Apakah fallback punya alert threshold?
- Apakah fallback punya recovery path?
- Apakah fallback diuji?
- Apakah fallback punya owner?
- Apakah fallback bisa memperbesar overload?
- Apakah fallback memiliki dependency yang sama dengan primary path?
- Apakah fallback membuka risiko security?
- Apakah fallback bisa menyebabkan keputusan irreversible?
- Apakah fallback dapat dimatikan dengan feature flag/config?
20. Latihan 20 Jam — Fallback & Degradation
Jam 1–3: Inventory
Ambil satu service Java. Buat daftar:
- dependency eksternal,
- dependency internal,
- operasi read,
- operasi write,
- enrichment optional,
- decision critical,
- side effect async.
Jam 4–6: Failure Matrix
Untuk tiap dependency, tulis:
- timeout,
- 5xx,
- connection refused,
- stale response,
- partial response,
- invalid response,
- unknown outcome.
Jam 7–10: Fallback Decision Record
Pilih lima operasi dan buat decision record.
Jam 11–14: Implement Explicit Degraded Result
Refactor satu endpoint agar tidak menyamarkan fallback sebagai normal response.
Jam 15–17: Instrumentation
Tambahkan:
- structured log,
- fallback metric,
- trace event,
- alert candidate.
Jam 18–20: Failure Injection
Matikan dependency, jalankan test, dan verifikasi:
- response benar,
- invariant tidak rusak,
- telemetry muncul,
- caller tahu degraded state,
- recovery berjalan.
21. Ringkasan
Fallback yang baik bukan trik availability. Fallback yang baik adalah kontrak produksi.
Mental model utama:
- Fallback adalah state, bukan catch block.
- Degradation adalah mode operasi, bukan bug yang disembunyikan.
- Default value hanya aman jika valid secara domain.
- Data stale harus punya freshness dan batas pemakaian.
- Authorization dan policy critical umumnya fail closed.
- Write critical sering lebih baik fail hard/read-only daripada best effort.
- Partial response harus eksplisit.
- Manual review adalah fallback sah untuk domain high-risk.
- Fallback harus observable.
- Fallback harus diuji sebelum incident.
Dalam part berikutnya, kita masuk ke fondasi lifecycle yang membuat fallback, shutdown, async flow, dan cleanup bisa benar: cancellation, interruption, dan cleanup.
You just completed lesson 15 in build core. 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.