Start HereOrdered learning track

Distributed Boundary Thinking

Learn Java Microservices Communication - Part 002

Distributed boundary thinking untuk Java microservices: mengapa network bukan detail implementasi, bagaimana mendesain boundary, deadline, failure, ownership, dan observability.

13 min read2457 words
PrevNext
Lesson 0296 lesson track01–17 Start Here
#java#microservices#communication#distributed-systems+1 more

Part 002 — Distributed Boundary Thinking

Target part ini: membongkar asumsi keliru bahwa remote call adalah method call yang kebetulan lewat network.

Network bukan transport detail. Network adalah bagian dari domain failure model.


1. Core Idea

Di microservices, boundary antar service bukan sekadar package boundary yang dipisah deployment. Boundary itu memisahkan:

  • process;
  • memory;
  • transaction;
  • clock;
  • failure domain;
  • deployment lifecycle;
  • ownership;
  • observability;
  • security context;
  • operational responsibility.

Karena itu, desain komunikasi antar service harus dimulai dari boundary, bukan dari client library.

Di Java, terlalu mudah membuat remote dependency terlihat seperti local dependency:

riskService.calculate(caseId);

Padahal remote dependency punya properti yang tidak dimiliki local method:

  • bisa gagal walaupun kode benar;
  • bisa lambat walaupun dependency sehat;
  • bisa berhasil di callee tetapi gagal terlihat di caller;
  • bisa dipanggil ulang oleh retry;
  • bisa melewati versi contract yang berbeda;
  • bisa terkena DNS, TLS, proxy, mesh, gateway, broker, atau load balancer;
  • bisa menimbulkan cascading failure.

Mental model utama: remote boundary adalah kontrak antara dua sistem yang bisa gagal secara independen.


2. Local Call vs Remote Call

DimensiLocal method callRemote service call
AddressingObject referenceDNS, service discovery, host, port, route
LatencyNanosecond/microsecondMillisecond/second/tail latency
FailureException karena bug/resource lokalTimeout, network error, partial failure, overload, protocol error
TransactionBisa satu transaction/memory contextTerpisah; tidak atomic secara natural
Type safetyCompiler-enforcedContract/schema/runtime compatibility
VersionSatu deployment unitBanyak deployment unit hidup bersamaan
ObservabilityStack trace lokalDistributed trace, logs, metrics, correlation id
RetryJarang perluSering perlu, tetapi berbahaya tanpa idempotency
CancellationThread interruption/internal signalDeadline, cancellation token, context propagation
OwnershipSatu codebase/team bisa samaSering beda team, beda lifecycle, beda SLO

Kesalahan desain terjadi ketika kita memperlakukan kolom kanan seperti kolom kiri.


3. Fallacies sebagai Checklist, Bukan Slogan

Distributed systems memiliki sekumpulan asumsi yang sering salah. Dalam praktik microservices modern, gunakan ini sebagai checklist desain:

FallacyPertanyaan produksi
Network reliableApa yang terjadi saat dependency unreachable?
Latency zeroBerapa latency budget dan p99 target?
Bandwidth infiniteApakah payload terlalu besar? Apakah compression perlu?
Network secureApakah identity, mTLS, token propagation, dan authorization jelas?
Topology staticApakah client tahan terhadap scaling, rolling deploy, DNS change?
One administratorSiapa operator dependency? Bagaimana escalation incident?
Transport cost zeroBerapa cost request fan-out, serialization, broker, egress?
Network homogeneousApakah dev, staging, prod, region, dan legacy link berbeda?

Yang penting bukan menghafal daftar ini. Yang penting adalah memaksa desain menjawab realita production.


4. Boundary Taxonomy

Satu service boundary sebenarnya terdiri dari banyak boundary sekaligus.

4.1 Network Boundary

Network boundary berarti caller tidak mengendalikan jalur komunikasi sepenuhnya.

Risiko:

  • DNS stale;
  • connection pool rusak;
  • TLS handshake mahal;
  • packet loss;
  • load balancer idle timeout;
  • proxy reset;
  • service mesh policy berubah;
  • latency spike antar zone/region.

Design response:

  • timeout eksplisit;
  • retry terbatas dengan jitter;
  • connection pool tuning;
  • circuit breaker;
  • observability per dependency;
  • fallback yang benar secara domain.

4.2 Transaction Boundary

Service lain tidak ikut transaction database service caller.

Buruk:

@Transactional
public void approveCase(CaseId caseId) {
    caseRepository.approve(caseId);
    notificationClient.sendApprovalEmail(caseId); // remote call inside transaction
}

Masalah:

  • transaction terbuka sambil menunggu network;
  • lock database lebih lama;
  • jika email berhasil tetapi transaction rollback, dunia luar melihat fakta palsu;
  • jika transaction commit tetapi call gagal, efek samping hilang.

Lebih sehat:

@Transactional
public void approveCase(CaseId caseId) {
    caseRepository.approve(caseId);
    outboxRepository.save(CaseApprovedEvent.from(caseId));
}

Efek samping eksternal diterbitkan dari outbox setelah state lokal commit.

4.3 Consistency Boundary

Setiap boundary memaksa kita memilih consistency model.

Pertanyaan:

  • Apakah caller butuh state terbaru sekarang?
  • Apakah stale read boleh?
  • Berapa lama eventual consistency yang dapat diterima?
  • Apakah user interface harus menampilkan status processing?
  • Apakah audit/regulatory evidence boleh tertunda?

Contoh:

Use caseConsistency needCommunication shape
Evaluate eligibility before approvalStrong enough for decision timeCall
Send notification after approvalEventualEvent/message
Build analytics projectionEventual/replayableEvent stream
Show case detail pageOften read model/cache acceptableQuery/API/read model
Append audit evidenceMust not be lost; may be async if durableOutbox + event/audit sink

4.4 Schema Boundary

Schema boundary berarti producer dan consumer bisa hidup dengan versi berbeda.

Risiko:

  • field dihapus;
  • enum value baru tidak dikenali;
  • required field ditambah;
  • semantic field berubah;
  • generated client belum diperbarui;
  • event lama tidak bisa dibaca setelah replay.

Design response:

  • backward/forward compatibility;
  • additive change;
  • tolerant reader;
  • explicit versioning jika perlu;
  • contract test;
  • schema registry/validation untuk event stream tertentu.

4.5 Ownership Boundary

Remote service punya owner. Owner bisa punya prioritas, deploy cadence, SLO, dan roadmap berbeda.

Pertanyaan penting:

  • Siapa yang boleh mengubah contract?
  • Siapa yang menerima alert jika dependency lambat?
  • Apakah caller boleh bergantung pada detail internal callee?
  • Apakah callee menjamin p99 latency tertentu?
  • Apa deprecation policy?

Boundary yang tidak punya ownership jelas akan berubah menjadi konflik organisasi.

4.6 Security Boundary

Service boundary sering juga security boundary.

Yang perlu dipikirkan:

  • service identity;
  • caller authentication;
  • authorization decision;
  • tenant context;
  • actor/user context;
  • propagation vs re-evaluation;
  • auditability;
  • data minimization.

Part ini tidak membahas auth/authz secara dalam karena sudah menjadi seri terpisah. Tetapi dalam communication design, metadata security tidak boleh dianggap optional.

4.7 Observability Boundary

Remote call tanpa observability adalah black box.

Minimum yang harus ada:

  • correlation id;
  • trace id/span id;
  • dependency name;
  • route/method/topic;
  • status/error class;
  • latency histogram;
  • retry count;
  • timeout count;
  • circuit breaker state;
  • queue lag untuk message/stream;
  • DLQ count;
  • payload size distribution.

Jika tidak bisa diamati, tidak bisa dioperasikan.


5. Deadline Thinking

Timeout sering diset sebagai angka ajaib:

risk-service.timeout: 30s

Ini biasanya tanda desain belum matang. Timeout harus berasal dari deadline budget.

Misalnya public API punya SLO internal: p95 harus di bawah 800 ms.

Budget kasar:

Dari sini, RiskService tidak boleh diberi timeout 30 detik. Jika caller hanya punya 250 ms budget, timeout 30 detik hanyalah cara menahan thread sampai sistem overload.

Deadline vs Timeout

  • Timeout: durasi maksimum untuk satu operasi.
  • Deadline: batas waktu absolut untuk seluruh pekerjaan.

Deadline lebih kuat karena bisa dipropagasikan lintas hop.

public record Deadline(Instant expiresAt) {
    public Duration remaining(Clock clock) {
        return Duration.between(clock.instant(), expiresAt).isNegative()
            ? Duration.ZERO
            : Duration.between(clock.instant(), expiresAt);
    }
}

Client remote harus membaca remaining budget:

Duration remaining = deadline.remaining(clock);

if (remaining.isZero()) {
    return new RemoteResult.TimedOut<>("risk-service");
}

riskHttpClient.call(request, remaining.minusMillis(25));

Part berikutnya akan membahas timeout detail. Di sini yang penting: boundary harus punya budget.


6. Cascading Failure Model

Cascading failure terjadi ketika satu dependency melambat, lalu caller menahan resource, lalu caller ikut melambat, lalu upstream juga melambat.

Jika semua caller melakukan retry agresif, downstream yang sudah overload menerima traffic tambahan.

Desain boundary harus mencegah loop ini dengan:

  • timeout pendek sesuai budget;
  • retry hanya untuk error transient dan idempotent;
  • exponential backoff + jitter;
  • circuit breaker;
  • bulkhead;
  • rate limit;
  • fallback/fail-fast;
  • load shedding.

7. Remote Call Contract Harus Memuat Failure Semantics

Contract bukan hanya request/response schema. Contract juga harus menjelaskan failure.

Minimal contract untuk remote API:

AreaIsi
Success semanticsApa arti sukses? Apakah state berubah?
Error classificationValidation, conflict, unavailable, timeout, rate limited, internal
RetriabilityError mana yang boleh di-retry?
IdempotencyKey apa yang dipakai untuk dedup command?
Timeout expectationp50/p95/p99 latency dan batas maksimum
ConsistencyData fresh atau bisa stale?
CompatibilityField mana optional, enum evolution, version policy
ObservabilityHeader/context wajib
SecurityCaller identity, tenant, actor, scope

Jika contract hanya OpenAPI/proto tanpa failure semantics, consumer akan menebak. Tebakan consumer berubah menjadi bug production.


8. Boundary Design Method

Gunakan langkah ini sebelum implementasi.

Step 1 — Nyatakan Outcome

Contoh:

Saat officer submit case, sistem harus memberi jawaban apakah case diterima untuk diproses. Notification dan analytics boleh tertunda.

Outcome ini langsung memisahkan sync dan async.

Step 2 — Pisahkan Decision Dependency dan Side Effect

InteractionButuh untuk decision?Shape
Validate case completenessYesLocal/domain validation
Check applicant eligibilityYesCall
Calculate risk scoreYes, if escalation immediateCall
Send notificationNoEvent/message
Append analyticsNoEvent
Generate PDFNoCommand message

Step 3 — Tentukan Failure Policy

InteractionFailure policy
Eligibility unavailableReject with retryable error or mark pending, depending product rule
Risk unavailableFail fast or fallback to manual review only if policy allows
Notification unavailableRetry async; do not fail submit
Analytics unavailableBuffer/retry; do not fail submit
Audit unavailableIf audit is mandatory evidence, persist local outbox before success

Step 4 — Tentukan Observability

Untuk setiap boundary:

  • metric name;
  • trace span name;
  • log fields;
  • alert condition;
  • dashboard slice;
  • runbook link.

Step 5 — Tentukan Evolution Policy

  • Bagaimana field baru ditambah?
  • Berapa lama version lama didukung?
  • Siapa approve breaking change?
  • Bagaimana replay event lama diuji?

9. Java Implementation Boundary Pattern

Buat remote boundary terlihat eksplisit di kode.

9.1 Interface Boundary

public interface RiskAssessmentClient {
    RemoteResult<RiskAssessment> assess(
        AssessRiskRequest request,
        Deadline deadline,
        CorrelationContext correlationContext
    );
}

9.2 Request Object

public record AssessRiskRequest(
    CaseId caseId,
    PersonId subjectId,
    CaseType caseType,
    BigDecimal claimedAmount,
    Instant submittedAt
) {}

9.3 Correlation Context

public record CorrelationContext(
    String traceId,
    String correlationId,
    String causationId,
    String tenantId,
    String actorId
) {}

9.4 Dependency Policy

public record DependencyPolicy(
    Duration connectTimeout,
    Duration responseTimeout,
    int maxAttempts,
    Duration baseBackoff,
    boolean circuitBreakerEnabled
) {}

9.5 Explicit Error Result

public sealed interface RemoteResult<T> {
    record Success<T>(T value) implements RemoteResult<T> {}
    record InvalidRequest<T>(String code, String message) implements RemoteResult<T> {}
    record Conflict<T>(String code, String message) implements RemoteResult<T> {}
    record RateLimited<T>(Duration retryAfter) implements RemoteResult<T> {}
    record DependencyUnavailable<T>(String dependency, boolean retryable) implements RemoteResult<T> {}
    record TimedOut<T>(String dependency) implements RemoteResult<T> {}
    record Unexpected<T>(String dependency, String diagnosticId) implements RemoteResult<T> {}
}

Tujuannya bukan memaksa semua project memakai pattern yang sama. Tujuannya adalah menghilangkan ilusi bahwa remote call sama dengan local method.


10. Jangan Membocorkan Transport ke Domain

Domain service tidak boleh tahu detail HTTP status, Kafka offset, atau gRPC status kecuali memang boundary layer.

Buruk:

public CaseDecision submitCase(SubmitCaseCommand command) {
    ResponseEntity<RiskResponse> response = riskRestClient.post(command);

    if (response.getStatusCode().value() == 503) {
        throw new RuntimeException("Risk unavailable");
    }

    return decide(response.getBody());
}

Lebih baik:

public CaseDecision submitCase(SubmitCaseCommand command) {
    RemoteResult<RiskAssessment> result = riskClient.assess(
        AssessRiskRequest.from(command),
        Deadline.after(Duration.ofMillis(250), clock),
        correlation.current()
    );

    return switch (result) {
        case RemoteResult.Success<RiskAssessment> success -> decide(success.value());
        case RemoteResult.TimedOut<RiskAssessment> ignored -> manualReview("RISK_TIMEOUT");
        case RemoteResult.DependencyUnavailable<RiskAssessment> ignored -> manualReview("RISK_UNAVAILABLE");
        case RemoteResult.InvalidRequest<RiskAssessment> invalid -> reject(invalid.code());
        default -> manualReview("RISK_UNKNOWN");
    };
}

Transport-specific mapping berada di adapter/client layer.


11. Boundary Smells

Gunakan daftar ini saat review desain.

11.1 Remote Call di Dalam Loop

for (CaseId caseId : caseIds) {
    riskClient.assess(caseId);
}

Jika caseIds berisi 1000 item, ini bukan loop biasa. Ini 1000 network calls. Solusi bisa berupa batch API, async pipeline, precomputed read model, atau stream.

11.2 Remote Call di Dalam Database Transaction

Sudah dibahas: ini memperpanjang lock dan membuat efek samping sulit dikendalikan.

11.3 Remote Call untuk Data yang Harusnya Local Projection

Jika page detail selalu memanggil 8 service untuk render, mungkin perlu read model/projection.

11.4 Retry di Banyak Layer

Retry bisa terjadi di:

  • client code;
  • HTTP library;
  • service mesh;
  • gateway;
  • broker;
  • SDK;
  • job scheduler.

Jika semua layer retry, satu request bisa menjadi puluhan request.

11.5 Tidak Ada Deadline Propagation

Setiap service memberi timeout sendiri tanpa tahu budget upstream. Hasilnya request yang sudah tidak berguna tetap diproses downstream.

11.6 Error Semantics Kabur

Semua error menjadi 500 atau RuntimeException. Caller tidak bisa membedakan invalid input, conflict, rate limit, timeout, dan dependency unavailable.

11.7 Contract Mengikuti Database

Jika event/API berubah setiap kali tabel berubah, boundary terlalu dekat dengan persistence model.


12. Boundary Review Template

Sebelum merge desain komunikasi baru, isi template ini.

## Boundary Review

### 1. Interaction
- Caller:
- Callee/consumer:
- Primitive: Call / Message / Event / Stream
- Business intent:

### 2. Criticality
- User-facing? Yes/No
- In critical path? Yes/No
- Required for decision? Yes/No
- Regulatory/audit impact? Yes/No

### 3. Contract
- Request/event schema:
- Response schema:
- Error model:
- Compatibility rule:
- Owner:

### 4. Failure Policy
- Timeout/deadline:
- Retry policy:
- Idempotency key:
- Fallback:
- DLQ/replay:

### 5. Observability
- Metrics:
- Trace span:
- Required log fields:
- Alert:
- Dashboard:
- Runbook:

### 6. Operational Risk
- Expected QPS/throughput:
- Payload size:
- Fan-out:
- Backpressure strategy:
- Deployment/version risk:

Template seperti ini lebih bernilai daripada diagram cantik yang tidak menjelaskan failure.


13. Example: Submit Enforcement Case

13.1 Requirement

Officer submit enforcement case. Sistem harus:

  • validasi completeness;
  • cek eligibility subject;
  • hitung initial risk;
  • simpan case;
  • catat audit evidence;
  • kirim notification;
  • update analytics;
  • trigger PDF generation.

13.2 Boundary Classification

InteractionPrimitiveRationale
CaseService -> EligibilityServiceCallDibutuhkan untuk menerima/menolak submit.
CaseService -> RiskServiceCallDibutuhkan jika initial route/escalation ditentukan saat submit.
CaseService -> AuditEvent/outboxEvidence wajib tidak hilang, tetapi tidak harus remote call sinkron.
CaseService -> NotificationEvent/messageTidak boleh menggagalkan submit.
CaseService -> AnalyticsEvent streamConsumer independen dan replayable.
CaseService -> PdfServiceCommand messagePekerjaan async, mahal, retryable.

13.3 Healthy Flow

13.4 Failure Policy

FailureResponse
Eligibility timeoutReturn retryable submit failure or pending review, depending business rule.
Risk timeoutRoute to manual review only if policy allows. Do not silently approve.
CaseDB failureSubmit fails. No event emitted.
Outbox insert failureSubmit fails because evidence/event consistency cannot be guaranteed.
Broker down after commitOutbox retains event; relay retries.
Notification failsRetry async; do not affect submitted case.
PDF generation failsDLQ/parking lot; user sees PDF pending.

Ini adalah contoh boundary thinking: bukan hanya memilih sync/async, tetapi menentukan efek failure terhadap outcome bisnis.


14. Production Boundary Invariants

Gunakan invariants berikut sebagai standar minimum.

Invariant 1 — Remote dependency harus punya timeout eksplisit

Tidak boleh ada remote call tanpa timeout.

Invariant 2 — Timeout harus berasal dari caller budget

Bukan angka default library.

Invariant 3 — Retry harus hanya untuk operasi yang aman diulang

Jika mutation tidak idempotent, jangan retry buta.

Invariant 4 — Remote call tidak boleh berada dalam database transaction kecuali alasan kuat dan terdokumentasi

Mayoritas kasus harus memakai outbox/event/message.

Invariant 5 — Error harus diklasifikasikan

Caller perlu tahu perbedaan invalid input, conflict, timeout, unavailable, dan unexpected.

Invariant 6 — Boundary harus observable

Minimal ada metric, trace, log correlation, dan alert untuk dependency penting.

Invariant 7 — Contract evolution harus direncanakan

Breaking change tanpa migration path adalah incident yang tertunda.

Invariant 8 — Critical path harus pendek dan bisa dijelaskan

Jika sebuah request user melewati 12 service, desainnya harus dipertanyakan.


15. Ringkasan

Distributed boundary thinking adalah kemampuan melihat bahwa setiap interaksi antar service memisahkan failure domain. Boundary bukan detail deployment. Boundary mengubah cara kita mendesain transaction, consistency, schema, ownership, security, observability, dan recovery.

Di Java, jangan sembunyikan remote dependency sebagai method biasa. Buat boundary eksplisit melalui client interface, deadline, correlation context, error classification, idempotency, dan dependency policy.

Part berikutnya akan masuk ke taxonomy komunikasi lebih sistematis: request/response, fire-and-forget, pub/sub, queue, log, stream, dan kapan masing-masing benar-benar layak dipakai.


References

Lesson Recap

You just completed lesson 02 in start here. 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.