Start HereOrdered learning track

Domain Model, State, and Workflow Boundaries

Learn Production Grade Contract-First Java Orchestration Platform - Part 004

Pemodelan domain, state, dan workflow boundary untuk platform orkestrasi kasus production-grade agar domain state, process state, persistence state, event state, audit state, SLA state, dan operational state tidak tercampur.

19 min read3735 words
PrevNext
Lesson 0440 lesson track0108 Start Here
#java#domain-modeling#state-machine#workflow+6 more

Part 004 — Domain Model, State, and Workflow Boundaries

Banyak sistem enterprise gagal bukan karena engineer tidak tahu Java, Kafka, PostgreSQL, atau Kubernetes. Mereka gagal karena satu konsep sederhana dicampur aduk:

state

Di sistem nyata, “state” bukan satu benda. Minimal ada beberapa jenis state:

  • domain state;
  • workflow state;
  • persistence state;
  • event state;
  • audit state;
  • SLA state;
  • integration state;
  • operational state;
  • UI/projection state.

Jika semua state dicampur ke satu field status, sistem akan terlihat sederhana di awal, lalu menjadi rapuh saat ada exception, appeal, cancellation, retry, migration, replay, atau human task yang berjalan paralel.

Part ini membangun mental model yang akan dipakai sampai akhir seri.

Targetnya: kita bisa menjawab dengan presisi:

  • case_status harus disimpan di mana?
  • BPMN token Camunda mewakili apa?
  • Kafka event mewakili state atau fakta?
  • Audit trail harus mengikuti domain state atau command attempt?
  • SLA breach itu status case atau status operasional?
  • Retry consumer boleh mengubah state yang sama berapa kali?
  • Kapan state transition harus atomic di PostgreSQL?
  • Kapan workflow cukup mengorkestrasi tanpa menjadi source of truth?

1. Problem: “Status” yang Terlalu Banyak Arti

Misalkan ada field:

status = IN_REVIEW

Apa artinya?

Mungkin berarti:

  1. officer sedang melakukan assessment;
  2. Camunda process sedang berhenti di user task review;
  3. case belum boleh diberi decision;
  4. SLA assessment sedang berjalan;
  5. event CaseReviewStarted sudah dipublish;
  6. UI harus menampilkan tombol submit assessment;
  7. audit terakhir menunjukkan assignment ke officer;
  8. database row belum closed;
  9. external referral masih waiting acknowledgment.

Jika semua makna itu ditempel ke satu field, field tersebut menjadi overloaded.

Overloaded state menghasilkan bug seperti ini:

  • UI menampilkan aksi yang salah;
  • Camunda process menunggu task yang sudah tidak relevan;
  • event replay mengembalikan case ke status lama;
  • query dashboard menghitung case salah;
  • audit tidak bisa menjelaskan kenapa status berubah;
  • migration process gagal karena state tidak punya makna tunggal;
  • recovery operator tidak tahu apakah harus memperbaiki domain row, Camunda instance, atau Kafka offset.

Solusinya bukan membuat 50 field status. Solusinya adalah memisahkan jenis state berdasarkan pertanyaan yang dijawab.


2. State Taxonomy

Gunakan taxonomy ini sebagai peta.

Jenis statePertanyaan yang dijawabSource of truth
Domain state“Secara bisnis/legal, case ini berada di tahap apa?”PostgreSQL domain tables.
Workflow state“Process engine sedang menunggu aktivitas apa?”Camunda 7 runtime/history tables.
Persistence state“Row mana yang committed, locked, version berapa?”PostgreSQL transaction + MVCC.
Event state“Fakta apa yang sudah dipublikasikan/diterima?”Outbox/inbox + Kafka log.
Audit state“Siapa melakukan apa, kapan, kenapa, dan akibatnya?”Append-only audit tables.
SLA state“Deadline apa yang berjalan, warning/breach apa yang terjadi?”Domain/SLA tables + timer process.
Integration state“External system sudah dikirim/diterima/acknowledged?”Integration tables + Kafka offsets.
Operational state“Sistem sehat, lag, incident, retry, repair apa?”Metrics/logs/traces/ops tables.
Projection/UI state“Apa yang nyaman ditampilkan/dicari user?”Read model/projection/query tables.

Satu kejadian bisnis bisa menyentuh beberapa state sekaligus, tetapi tiap state punya pemilik dan semantics berbeda.


3. Domain State

Domain state menjawab pertanyaan bisnis.

Contoh domain state utama:

DRAFT
REGISTERED
UNDER_ASSESSMENT
ASSESSMENT_COMPLETED
UNDER_INVESTIGATION
PENDING_LEGAL_REVIEW
DECISION_ISSUED
CLOSED
CANCELLED

State ini harus:

  • punya definisi bisnis;
  • punya allowed transition;
  • punya invariant;
  • bisa diaudit;
  • bisa di-query tanpa membaca workflow engine;
  • tidak bergantung pada detail implementasi Camunda.

3.1 Domain state machine awal

State machine ini bukan sekadar diagram. Ia adalah kontrak domain.

Jika command issueDecision dipanggil ketika status masih UNDER_ASSESSMENT, sistem harus menolak. Penolakan ini tidak boleh tergantung apakah BPMN token sedang berada di gateway tertentu. Domain harus menjaga invariantnya sendiri.

3.2 Invariant domain

Contoh invariant:

Case tidak boleh CLOSED sebelum decision dibuat.
Case tidak boleh DECISION_ISSUED tanpa at least satu decision record valid.
Case tidak boleh UNDER_INVESTIGATION jika assessment belum completed.
Case tidak boleh memiliki dua active final decision.
Case number harus unik.
Closed case tidak boleh menerima evidence baru kecuali lewat reopen process eksplisit.
Decision severe sanction wajib legal review.

Invariant harus ditegakkan di beberapa level:

LevelContoh
Java domain policycanIssueDecision(currentStatus, decisionType)
PostgreSQL constraintunique active decision, FK, NOT NULL, CHECK
Transaction lockoptimistic/pessimistic lock saat transition
Testtransition matrix, property-like negative cases

Jangan hanya mengandalkan validasi UI atau BPMN gateway. UI bisa bypass. BPMN bisa incident. Consumer bisa replay. Operator bisa menjalankan repair. Domain invariant tetap harus bertahan.


4. Workflow State

Workflow state menjawab pertanyaan process engine:

Process instance ini sedang berada di aktivitas apa?
Job apa yang pending?
Timer apa yang aktif?
User task apa yang menunggu?
Incident apa yang terjadi?

Contoh workflow state:

  • process instance active;
  • token berada di UserTask_AssessCase;
  • timer boundary AssessmentSlaWarning aktif;
  • service task EnrichEntityData failed;
  • process instance menunggu message EvidenceSubmitted;
  • incident tercipta karena delegate exception.

Workflow state bukan sumber utama status bisnis.

4.1 Mapping domain state ke workflow state

Mapping bisa seperti ini:

Domain stateWorkflow state yang mungkin
REGISTEREDprocess baru dimulai, initial validation service task, atau incident di validation.
UNDER_ASSESSMENTuser task assessment aktif, timer SLA assessment aktif.
ASSESSMENT_COMPLETEDgateway menuju investigation/legal review, atau message wait.
UNDER_INVESTIGATIONinvestigation user task aktif, evidence wait, external data wait, timer aktif.
PENDING_LEGAL_REVIEWlegal review user task aktif.
DECISION_ISSUEDclose case service task, notification task, event publication wait.
CLOSEDprocess instance ended atau archival subprocess.

Perhatikan: satu domain state bisa punya beberapa workflow state internal.

Karena itu jangan membuat domain status terlalu granular hanya agar cocok dengan BPMN step.

Bad:

CASE_STATUS = WAITING_AT_BPMN_TASK_REVIEW_EVIDENCE_V2

Better:

case.status = UNDER_INVESTIGATION
workflow.activeTask = Review Evidence

4.2 BPMN token sebagai operational truth

BPMN token adalah kebenaran untuk process execution, bukan kebenaran legal domain.

Jika operator bertanya:

Kenapa case ini tidak lanjut?

Camunda berguna:

Ada incident di service task ValidateEvidenceBundle.

Jika auditor bertanya:

Apakah case ini sudah memiliki legal decision final?

Domain database dan audit trail yang menjawab.


5. Persistence State

Persistence state menjawab pertanyaan teknis database:

  • row sudah committed atau belum;
  • transaction isolation apa;
  • row version berapa;
  • lock siapa yang memegang;
  • constraint apa yang gagal;
  • migration version apa;
  • data visible untuk transaction mana.

Dalam PostgreSQL, concurrency bukan hanya “dua thread update row”. Ada MVCC, isolation level, locks, deadlocks, serialization failure, dan transaction boundary.

5.1 Version sebagai state teknis

Contoh field:

version bigint NOT NULL DEFAULT 0

version bukan domain state. Ia persistence state untuk optimistic locking.

Command update bisa memakai:

UPDATE case_domain.enforcement_case
SET status = #{newStatus},
    version = version + 1,
    updated_at = now()
WHERE case_id = #{caseId}
  AND version = #{expectedVersion}
  AND status = #{expectedStatus};

Jika affected row = 0, kemungkinan:

  • case tidak ada;
  • version stale;
  • status sudah berubah;
  • command tidak valid lagi.

Aplikasi harus membedakan error ini secukupnya.

5.2 Lock sebagai state sementara

Lock tidak terlihat di domain model, tetapi mempengaruhi behavior.

Contoh operasi yang butuh lock:

  • assign officer;
  • issue decision;
  • close case;
  • process same Kafka event;
  • publish outbox batch;
  • repair stuck process.

Kita akan memakai kombinasi:

  • optimistic locking untuk command biasa;
  • SELECT ... FOR UPDATE untuk transition kritikal;
  • FOR UPDATE SKIP LOCKED untuk worker batch;
  • advisory lock untuk koordinasi khusus jika perlu.

Persistence state harus dipahami agar kita tidak membuat bug yang hanya muncul di production load.


6. Event State

Event state menjawab:

Fakta apa yang sudah terjadi dan dikomunikasikan ke luar boundary?

Event bukan command. Event adalah fakta masa lalu.

Good:

CaseRegistered
AssessmentCompleted
InvestigationStarted
DecisionIssued
CaseClosed
SlaBreached

Bad:

StartAssessmentPlease
DoDecisionNow
UpdateCaseStatus
CallNotificationService

6.1 Event harus berasal dari committed fact

Event CaseRegistered hanya boleh dipublish jika case benar-benar committed di database.

Karena itu event publication tidak boleh dilakukan langsung dari memory sebelum commit.

Flow benar:

Event state yang kita simpan:

FieldMakna
event_idIdentitas global event.
aggregate_typeMisalnya case.
aggregate_idcase_id.
event_typeCaseRegistered.
event_versionVersi schema event.
payloadIsi event.
statusNEW, PUBLISHED, FAILED, dll.
attempt_countJumlah percobaan publish.
published_atWaktu publish berhasil.

6.2 Event replay tidak boleh merusak domain

Jika event dikonsumsi ulang, consumer harus aman.

Contoh inbox:

CREATE TABLE case_integration.inbox_event (
    event_id          uuid PRIMARY KEY,
    topic             text NOT NULL,
    partition_no      int NOT NULL,
    offset_no         bigint NOT NULL,
    event_type        text NOT NULL,
    received_at       timestamptz NOT NULL,
    processed_at      timestamptz,
    processing_status text NOT NULL
);

Consumer melakukan:

insert event_id ke inbox
jika duplicate -> skip
jika insert berhasil -> process side-effect
commit

Event state bukan hanya Kafka offset. Offset adalah posisi consumer. Inbox adalah bukti bahwa side-effect sudah pernah diterapkan.


7. Audit State

Audit state menjawab:

Apa yang terjadi, siapa yang melakukan, kapan, dari mana, terhadap entity apa, dan kenapa?

Audit berbeda dari log.

Log:

2026-07-02 INFO Decision submitted

Audit:

actor=officer-123
command=IssueDecision
caseId=...
previousStatus=PENDING_LEGAL_REVIEW
newStatus=DECISION_ISSUED
decisionType=SANCTION
reasonCode=EVIDENCE_CONFIRMED
correlationId=...
occurredAt=...

Audit harus bisa dipakai untuk pembelaan keputusan. Dalam sistem regulatory, audit bukan ornamen.

7.1 Audit sebagai append-only fact

Audit entry tidak boleh diedit sembarangan. Jika perlu koreksi, tambahkan audit entry koreksi.

Contoh audit table:

CREATE TABLE case_domain.case_audit_entry (
    audit_id        uuid PRIMARY KEY,
    case_id         uuid NOT NULL,
    actor_type      text NOT NULL,
    actor_id        text NOT NULL,
    action          text NOT NULL,
    reason_code     text,
    before_snapshot jsonb,
    after_snapshot  jsonb,
    correlation_id  text NOT NULL,
    command_id      text NOT NULL,
    occurred_at     timestamptz NOT NULL
);

Audit state bisa memiliki snapshot, tetapi snapshot bukan pengganti domain table. Snapshot membantu menjelaskan perubahan pada titik waktu tertentu.

7.2 Audit command attempt vs audit success

Tidak semua command attempt menjadi domain change.

Pertanyaan desain:

Apakah command yang ditolak perlu audit?

Untuk sistem regulatory, sering kali iya, terutama untuk aksi sensitif.

Kita bisa memisahkan:

Audit typeContoh
Security/access auditUser mencoba aksi tanpa izin.
Command attempt auditUser mencoba issue decision tetapi data belum lengkap.
Domain change auditDecision berhasil diterbitkan.
Operational auditOperator menjalankan repair/replay.

Jangan mencampur semuanya ke satu semantic tanpa field pembeda.


8. SLA State

SLA state menjawab:

Deadline apa yang berlaku, apakah sudah warning, apakah sudah breach, siapa yang harus diberitahu, dan apakah breach sudah ditangani?

SLA bukan selalu domain status.

Case bisa tetap UNDER_INVESTIGATION dan sekaligus punya SLA state BREACHED.

case.status = UNDER_INVESTIGATION
sla.assessment.status = COMPLETED
sla.investigation.status = BREACHED

Jika SLA dipaksa masuk ke case_status, state machine akan meledak:

UNDER_INVESTIGATION_SLA_OK
UNDER_INVESTIGATION_SLA_WARNING
UNDER_INVESTIGATION_SLA_BREACHED
UNDER_INVESTIGATION_SLA_ESCALATED

Itu tanda state berbeda dicampur.

8.1 SLA model

SLA state bisa di-drive oleh:

  • domain command;
  • Camunda timer;
  • scheduled worker;
  • database query for due items.

Tetapi source of truth SLA sebaiknya ada di table yang bisa di-query dan diaudit.


9. Integration State

Integration state menjawab:

Kita sudah menerima apa dari external system?
Kita sudah mengirim apa?
External system sudah ack atau belum?
Retry terakhir kapan?
Payload versi berapa?

Contoh:

external_referral.status = RECEIVED
external_referral.case_id = ...
external_notification.status = SENT
external_notification.ack_status = WAITING

Integration state berbeda dari domain state.

Misalnya notification gagal dikirim. Apakah case status berubah?

Biasanya tidak.

case.status = DECISION_ISSUED
notification.status = FAILED_RETRYABLE

Jika notification gagal mengubah case status menjadi ERROR, domain menjadi tercemar oleh masalah integrasi.

Lebih baik operational/integration state yang menunjukkan failure, sementara domain tetap benar.


10. Operational State

Operational state menjawab:

Sistem sedang sehat atau tidak?
Apa yang stuck?
Apa yang lag?
Apa yang retry terus?
Apa yang butuh intervensi operator?

Contoh operational state:

  • outbox lag 15 menit;
  • Kafka consumer group lag 50000 records;
  • Camunda incidents 23;
  • DB connection pool 95% used;
  • API p95 latency 2.5s;
  • pod restart 12 kali dalam 10 menit;
  • migration version mismatch;
  • failed repair action.

Operational state tidak boleh mengubah domain state secara otomatis tanpa command/recovery yang eksplisit.

Bad:

Jika Kafka lag tinggi, set semua case menjadi PROCESSING_DELAYED.

Better:

Expose operational alert: event publication delayed.
Domain case tetap pada status bisnisnya.

Jika perlu memberi tahu user bahwa update mungkin terlambat, itu projection/UI concern, bukan domain status utama.


11. Projection/UI State

Projection state menjawab:

Data apa yang dibutuhkan UI/search/dashboard dengan cepat?

Projection boleh redundant.

Contoh:

case_search_projection
  case_id
  case_number
  status
  severity
  assigned_officer_name
  current_task_name
  sla_badge
  last_event_at

Projection bukan source of truth. Projection bisa dibangun ulang dari domain table, audit, workflow query, atau event.

Projection boleh eventual consistent selama UI memahami behavior-nya.


12. Boundary Rule: Siapa Boleh Mengubah Apa?

Gunakan matriks ini.

StateBoleh diubah olehTidak boleh diubah langsung oleh
Domain stateDomain command handler dalam transaksiUI, Kafka consumer tanpa command boundary, BPMN script sembarang.
Workflow stateCamunda engine melalui BPMN executionDomain repository langsung.
Persistence statePostgreSQL transaction/lock/migrationBusiness code tanpa memahami isolation.
Event stateOutbox publisher, consumer inboxAPI direct publish tanpa outbox.
Audit stateAudit appender dalam command boundaryManual update biasa.
SLA stateSLA policy, timer, command handlerUI status toggle.
Integration stateIntegration adapter/consumer/publisherDomain policy yang tidak tahu external protocol.
Operational stateObservability/ops/reconciliationDomain transition otomatis tanpa command.
Projection stateProjector/query updaterBusiness invariant.

Rule penting:

Setiap state punya pemilik. Jika semua komponen boleh mengubah semua state, tidak ada invariant yang benar-benar aman.

13. Command sebagai Satu-satunya Pintu Domain Transition

Domain state berubah melalui command.

Contoh command:

RegisterCase
StartAssessment
CompleteAssessment
StartInvestigation
SubmitEvidence
CompleteInvestigation
RequestLegalReview
IssueDecision
CloseCase
CancelCase
ReopenCase

Command punya:

  • command id;
  • actor;
  • target aggregate;
  • payload;
  • idempotency key;
  • correlation id;
  • expected version/status jika perlu;
  • reason code;
  • requested at.

Contoh Java record konseptual:

public record IssueDecisionCommand(
    UUID commandId,
    UUID caseId,
    String actorId,
    String actorRole,
    long expectedVersion,
    DecisionType decisionType,
    String reasonCode,
    String correlationId,
    Instant requestedAt
) {}

Command handler melakukan:

load current state
validate authorization
validate transition
apply domain policy
persist transition atomically
append audit
insert outbox
return result

13.1 Jangan biarkan setter mengubah state

Bad:

caseEntity.setStatus("CLOSED");
caseMapper.update(caseEntity);

Better:

caseCommandHandler.handle(new CloseCaseCommand(...));

Atau di domain object:

caseAggregate.close(closeCaseCommand, policy, clock);

State transition harus menjadi aksi bernama, bukan assignment field acak.


14. Transition Matrix

Sebelum coding, buat transition matrix.

FromCommandToGuard
REGISTEREDStartAssessmentUNDER_ASSESSMENTofficer assigned.
UNDER_ASSESSMENTCompleteAssessmentASSESSMENT_COMPLETEDassessment result complete.
ASSESSMENT_COMPLETEDStartInvestigationUNDER_INVESTIGATIONinvestigation required.
UNDER_INVESTIGATIONCompleteInvestigationPENDING_LEGAL_REVIEWrequired evidence complete.
PENDING_LEGAL_REVIEWIssueDecisionDECISION_ISSUEDlegal reviewer approved.
DECISION_ISSUEDCloseCaseCLOSEDnotification/outcome recorded.
REGISTEREDCancelCaseCANCELLEDcancellation reason valid.
UNDER_ASSESSMENTCancelCaseCANCELLEDsupervisor approval if assigned.

Negative transition harus dites:

FromInvalid commandExpected error
REGISTEREDIssueDecisionINVALID_STATE_TRANSITION
CLOSEDSubmitEvidenceCASE_ALREADY_CLOSED
UNDER_INVESTIGATIONStartAssessmentASSESSMENT_ALREADY_COMPLETED
CANCELLEDCloseCaseCASE_CANCELLED

Testing negative path lebih penting daripada happy path di sistem stateful.


15. Domain Event vs Integration Event

Tidak semua event sama.

15.1 Domain event

Domain event adalah fakta internal tentang aggregate.

CaseRegistered
CaseAssigned
AssessmentCompleted
DecisionIssued

Domain event biasanya dibuat saat domain command berhasil.

15.2 Integration event

Integration event adalah event yang dipublikasikan ke luar bounded context.

Ia bisa mirip domain event, tetapi sering perlu envelope, filtering, redaction, versioning, dan compatibility berbeda.

Contoh:

case.lifecycle.v1 CaseRegistered

Payload integration event mungkin tidak memuat semua detail internal karena privacy/security/compatibility.

15.3 Process event

Process event adalah event untuk menggerakkan workflow.

Contoh:

EvidenceSubmittedMessage
LegalReviewCompletedMessage
ExternalRegistryDataReceivedMessage

Ini bisa berasal dari domain event atau external event, tetapi dipakai untuk korelasi BPMN.

Jangan menyamakan semua event ini. Mereka punya audience berbeda.


16. Mapping Command, State, Audit, Event, Workflow

Untuk satu command CompleteAssessment, mapping-nya bisa seperti ini:

Dalam transaksi ideal:

domain update + audit + outbox + SLA update = atomic

BPMN correlation bisa dilakukan setelah commit dengan recovery path, atau dalam boundary yang sama jika architecture memilih coupling lebih kuat.

Yang penting: keputusan ini eksplisit, bukan kebetulan.


17. Camunda Variable Contract

Camunda variables harus minimal dan stabil.

Contoh variable contract:

{
  "caseId": "uuid",
  "caseNumber": "ENF-2026-000001",
  "severity": "HIGH",
  "assessmentGroup": "ENFORCEMENT_OFFICER",
  "slaDeadline": "2026-07-10T10:00:00Z"
}

Hindari:

{
  "case": { "entire nested domain object": "..." },
  "allEvidence": ["..."],
  "auditTrail": ["..."],
  "uiState": { "...": "..." }
}

Variable besar menciptakan masalah:

  • serialization issue;
  • migration sulit;
  • stale data;
  • history table membengkak;
  • process instance sulit dipahami;
  • sensitive data tersebar.

Rule:

Camunda variable adalah routing/process context, bukan domain object storage.

18. Business Key dan Correlation

Setiap process instance harus punya business key yang stabil.

Pilihan:

businessKey = caseId

Atau:

businessKey = caseNumber

Untuk sistem internal, caseId UUID biasanya lebih stabil. caseNumber berguna untuk manusia tetapi bisa punya format bisnis yang berubah.

Correlation rule:

Use caseCorrelation key
Start lifecycle processcaseId as business key.
Evidence submittedmessage name + caseId.
External registry data receivedmessage name + caseId or external reference mapping.
SLA timerprocess-local timer plus caseId.
Appeal submittednew process or subprocess with caseId + appealId.

Jangan correlate hanya berdasarkan status atau user id. Correlation harus deterministic.


19. State Drift

State drift terjadi ketika beberapa state yang harus sinkron mulai berbeda.

Contoh:

Database: case.status = REGISTERED
Camunda: no process instance exists
Outbox: CaseRegistered published
UI: shows UNDER_ASSESSMENT

Atau:

Database: case.status = DECISION_ISSUED
Camunda: still waiting at Legal Review task
Kafka: DecisionIssued not published
Audit: no decision audit entry

State drift tidak bisa dihindari sepenuhnya di distributed system. Yang penting adalah:

  • drift bisa dideteksi;
  • drift punya owner;
  • drift punya recovery path;
  • recovery idempotent;
  • audit mencatat recovery.

19.1 Reconciliation query

Contoh reconciliation:

Find cases where status = REGISTERED and no active process instance id after 5 minutes.
Find outbox events NEW older than 2 minutes.
Find Camunda incidents grouped by activity id.
Find cases DECISION_ISSUED without DecisionIssued outbox event.
Find SLA RUNNING with deadline < now and no breach event.

Reconciliation adalah bagian desain, bukan script darurat.


20. Error State vs Domain State

Jangan menambahkan ERROR ke domain status tanpa berpikir.

Banyak error adalah operational, bukan domain.

Contoh:

ErrorDomain status berubah?State yang berubah
Kafka publish gagalTidakoutbox status.
Email notification gagalTidaknotification/integration status.
Camunda service task incidentBiasanya tidakworkflow incident.
DB deadlock saat commandTidak jika rollbackcommand result error.
Evidence invalidBisa, tergantung ruledomain validation result/audit.
Legal reviewer rejects decisionYa, jika itu outcome bisnisdomain state/decision state.

Domain status ERROR sering menjadi tempat sampah untuk semua masalah. Hindari kecuali error itu benar-benar state bisnis.


21. Cancellation, Reopen, dan Appeal

State machine sederhana biasanya lupa tiga hal:

  • cancellation;
  • reopen;
  • appeal.

Dalam regulatory system, ini penting.

21.1 Cancellation

Cancellation bukan delete.

Cancellation harus punya:

  • reason;
  • actor;
  • allowed states;
  • audit;
  • effect ke workflow;
  • event;
  • downstream notification.

21.2 Reopen

Reopen lebih berbahaya daripada create.

Pertanyaan:

  • Dari status apa boleh reopen?
  • Apakah decision lama tetap valid?
  • Apakah reopen membuat investigation baru atau melanjutkan lama?
  • Apakah SLA dihitung ulang?
  • Apakah event CaseReopened cukup atau perlu InvestigationReopened?
  • Bagaimana audit menjelaskan pembatalan closure?

21.3 Appeal

Appeal bisa menjadi:

  • subprocess dalam case lifecycle;
  • process instance baru terkait case;
  • aggregate baru appeal dengan lifecycle sendiri.

Untuk sistem besar, appeal sering lebih bersih sebagai aggregate/process terpisah yang mereferensikan case.


22. Aggregate Boundary

Tidak semua table dalam satu aggregate.

Candidate aggregate:

AggregateAlasan
CaseRoot lifecycle utama.
EvidenceBundleBisa banyak, lifecycle validasi sendiri.
DecisionFinal/semi-final legal outcome, invariant kuat.
SlaTrackerDeadline dan escalation lifecycle.
ExternalReferralIntegration lifecycle dengan external agency.
AppealLifecycle sendiri setelah decision.

Aggregate boundary membantu menentukan transaksi.

Rule:

Satu command sebaiknya mengubah satu aggregate utama secara atomik.

Jika perlu mengubah beberapa aggregate, pertimbangkan:

  • apakah sebenarnya boundary aggregate terlalu kecil/besar;
  • apakah perlu domain service transactional;
  • apakah event-driven eventual consistency cukup;
  • apakah invariant lintas aggregate benar-benar harus synchronous.

23. Case Aggregate Conceptual Model

Model ini belum final. Ia memberi arah bahwa case root memiliki relasi ke beberapa sub-entity dan related aggregate.


24. Implementasi State Transition di Java

Kita ingin transition eksplisit.

Contoh sederhana:

public enum CaseStatus {
    REGISTERED,
    UNDER_ASSESSMENT,
    ASSESSMENT_COMPLETED,
    UNDER_INVESTIGATION,
    PENDING_LEGAL_REVIEW,
    DECISION_ISSUED,
    CLOSED,
    CANCELLED
}

Transition policy:

public final class CaseTransitionPolicy {

    public boolean canTransition(CaseStatus from, CaseCommandType command) {
        return switch (from) {
            case REGISTERED -> command == CaseCommandType.START_ASSESSMENT
                    || command == CaseCommandType.CANCEL_CASE;
            case UNDER_ASSESSMENT -> command == CaseCommandType.COMPLETE_ASSESSMENT
                    || command == CaseCommandType.CANCEL_CASE;
            case ASSESSMENT_COMPLETED -> command == CaseCommandType.START_INVESTIGATION
                    || command == CaseCommandType.REQUEST_LEGAL_REVIEW;
            case UNDER_INVESTIGATION -> command == CaseCommandType.COMPLETE_INVESTIGATION;
            case PENDING_LEGAL_REVIEW -> command == CaseCommandType.ISSUE_DECISION;
            case DECISION_ISSUED -> command == CaseCommandType.CLOSE_CASE;
            case CLOSED, CANCELLED -> false;
        };
    }
}

Ini masih sederhana. Production implementation perlu error detail, reason, actor permission, dan domain data completeness.

Yang penting: transition berada di tempat eksplisit dan dites.


25. Implementasi State Guard di PostgreSQL

Java policy penting, tetapi database tetap harus menjaga update tidak liar.

Contoh update guarded:

UPDATE case_domain.enforcement_case
SET status = 'DECISION_ISSUED',
    version = version + 1,
    updated_at = now()
WHERE case_id = #{caseId}
  AND status = 'PENDING_LEGAL_REVIEW'
  AND version = #{expectedVersion};

Lalu pastikan decision record ada dalam transaksi yang sama.

INSERT INTO case_domain.case_decision (
    decision_id,
    case_id,
    decision_type,
    decision_status,
    issued_at
) VALUES (
    #{decisionId},
    #{caseId},
    #{decisionType},
    'ISSUED',
    now()
);

Jika UPDATE affected row = 0, jangan lanjut insert decision seolah-olah berhasil.

25.1 Constraint untuk final decision

Contoh partial unique index:

CREATE UNIQUE INDEX ux_case_one_issued_decision
ON case_domain.case_decision(case_id)
WHERE decision_status = 'ISSUED';

Ini menjaga invariant: satu case hanya boleh punya satu issued decision aktif.


26. Workflow Boundary Pattern

Kita butuh port agar domain tidak bergantung langsung ke Camunda.

public interface CaseWorkflowPort {
    void startCaseLifecycle(UUID caseId, String caseNumber, CaseWorkflowContext context);
    void correlateAssessmentCompleted(UUID caseId, AssessmentWorkflowResult result);
    void correlateDecisionIssued(UUID caseId, DecisionWorkflowResult result);
}

Adapter Camunda:

public final class CamundaCaseWorkflowAdapter implements CaseWorkflowPort {
    // uses RuntimeService internally
}

Domain command handler bergantung ke CaseWorkflowPort, bukan RuntimeService.

Namun lebih baik lagi, untuk mengurangi coupling transaksi, command handler bisa menulis workflow_command table atau outbox internal, lalu worker melakukan correlation.

Pattern:

domain command -> insert workflow command -> commit -> workflow dispatcher -> Camunda RuntimeService

Ini membuat workflow integration idempotent dan recoverable.


27. State Ownership in Code

Susunan package konseptual:

case-domain-core
  CaseAggregate
  CaseStatus
  CaseCommandType
  CaseTransitionPolicy
  CaseInvariantViolation

case-domain-command
  RegisterCaseHandler
  CompleteAssessmentHandler
  IssueDecisionHandler
  CloseCaseHandler

case-persistence-mybatis
  CaseMapper
  CaseDecisionMapper
  CaseAuditMapper
  OutboxMapper

case-workflow-camunda7
  CamundaCaseWorkflowAdapter
  CamundaMessageCorrelationAdapter

case-event-publisher
  CaseEventFactory
  OutboxEventWriter

Dependency direction:

handler -> domain policy
handler -> repository port
handler -> audit port
handler -> outbox port
handler -> workflow port
adapter -> framework

Jangan biarkan mapper XML, BPMN variable name, atau Kafka topic name tersebar di domain core.


28. Edge Cases yang Harus Didesain Dari Awal

28.1 Double submit case

Client timeout lalu retry POST /v1/cases.

Solusi:

  • idempotency key wajib untuk command create;
  • idempotency record menyimpan request hash dan response reference;
  • duplicate dengan same key + same payload mengembalikan result sama;
  • same key + different payload ditolak.

28.2 Officer complete assessment dua kali

Dua tab browser atau double click.

Solusi:

  • command idempotency;
  • optimistic lock version;
  • guarded update by expected status;
  • audit hanya satu success, duplicate menjadi replay/ignored.

28.3 Kafka event datang sebelum process menunggu message

Event eksternal diterima cepat, tetapi BPMN belum sampai message catch event.

Solusi:

  • jangan langsung drop correlation failure;
  • simpan integration event;
  • retry correlation;
  • atau desain process supaya event ditangani domain dulu, process membaca state saat sampai step terkait.

28.4 Camunda incident setelah domain state berubah

Service task gagal setelah case status diupdate.

Solusi:

  • pastikan service task idempotent;
  • gunakan async boundary secara sadar;
  • recovery command membaca domain state;
  • jangan rollback domain fact yang sudah committed tanpa command kompensasi.

28.5 Migration mengubah status enum

Status PENDING_LEGAL_REVIEW ingin diganti menjadi LEGAL_REVIEW_PENDING.

Solusi:

  • jangan rename langsung;
  • tambahkan support read both;
  • migrate data;
  • update contract;
  • cleanup setelah semua code lama tidak berjalan;
  • pikirkan event payload lama.

29. Testing Strategy untuk State Boundary

State boundary harus dites dengan beberapa level.

29.1 Transition matrix test

Given REGISTERED
When IssueDecision
Then INVALID_STATE_TRANSITION

29.2 Repository guarded update test

Given status UNDER_ASSESSMENT version 3
When update expected status PENDING_LEGAL_REVIEW
Then affected rows = 0

29.3 Idempotency test

Given commandId already processed
When same command replayed
Then same result returned and no duplicate audit/outbox

29.4 Workflow correlation test

Given process instance businessKey=caseId
When AssessmentCompleted correlated
Then process moves to next expected activity

29.5 Event replay test

Given inbox has eventId
When Kafka delivers same event again
Then side-effect skipped

29.6 Reconciliation test

Given case REGISTERED without processInstanceId older than threshold
When reconciliation runs
Then workflow start command created idempotently

30. Decision Rules: Where Should Logic Live?

Gunakan tabel ini saat ragu.

LogicTempat utamaAlasan
Allowed case status transitionDomain policy + DB guardInvariant bisnis harus tahan retry/concurrency.
Unique case numberPostgreSQL constraintHarus benar walau race condition.
HTTP request shapeOpenAPI + validation layerTransport contract.
Evidence completeness ruleDomain policy, mungkin query DBRule bisnis.
SLA timer waitCamunda timer / schedulerProcess timing.
SLA breach recordDomain/SLA tableHarus bisa diaudit/query.
Human task assignmentWorkflow + domain assignment policyProcess + business ownership.
Kafka publish guaranteeOutboxTransactional reliability.
Consumer duplicate detectionInboxIdempotent side-effect.
Query dashboardProjection/query modelPerformance/read shape.
Repair actionOps command + auditControlled operational mutation.

Jika logic mempengaruhi legal/business correctness, jangan hanya taruh di BPMN atau UI.


31. Practical Mental Model

Gunakan model ini:

Domain state = what is true in the business.
Workflow state = what work is currently being coordinated.
Persistence state = what data is committed and protected by transactions.
Event state = what facts have been communicated asynchronously.
Audit state = what must be explainable later.
SLA state = what time obligation is active or breached.
Integration state = what external interaction has happened.
Operational state = what the system is doing or failing to do.
Projection state = what users can search or see efficiently.

Ketika ada field baru bernama status, jangan langsung tambahkan. Tanya dulu:

Status untuk menjawab pertanyaan apa?
Siapa pemiliknya?
Apa allowed transition-nya?
Apakah harus diaudit?
Apakah harus transactional?
Apakah boleh eventual consistent?
Apakah bisa di-rebuild?
Apakah external contract melihatnya?

32. Ringkasan

Part ini adalah fondasi desain yang akan menghemat banyak kesalahan di part implementasi.

Poin utama:

  • Jangan mencampur semua state ke satu status.
  • Domain state adalah source of truth bisnis dan legal.
  • Workflow state adalah state koordinasi proses Camunda.
  • Persistence state adalah realitas transaksi, lock, visibility, dan version.
  • Event state adalah fakta asynchronous yang harus aman dipublish dan di-replay.
  • Audit state adalah bukti, bukan sekadar log.
  • SLA state punya lifecycle sendiri.
  • Integration state tidak boleh mengotori domain state.
  • Operational state harus terlihat dan recoverable.
  • Projection state boleh redundant dan rebuildable.
  • State transition harus lewat command boundary, bukan setter sembarang.
  • BPMN mengorkestrasi; domain menjaga invariant.
  • PostgreSQL constraints dan guarded update adalah bagian dari domain protection.

Part berikutnya akan masuk ke Repository and Module Topology: bagaimana seluruh boundary ini diterjemahkan menjadi Maven multi-module layout, package structure, contract folder, generated source boundary, application modules, test modules, dan deployment artifacts.


Referensi Primer

  • PostgreSQL Documentation — transaction isolation, MVCC, explicit locking, constraints, indexes.
  • Camunda 7 Documentation — process engine, process instance, user task, job executor, timer, incident, transaction handling.
  • Apache Kafka Documentation — topics, partitions, producer/consumer semantics, records, consumer groups.
  • Eclipse Jersey User Guide — request/resource boundary, filters, providers, deployment model.
  • Apache Maven Documentation — project object model, build lifecycle, module management.
Lesson Recap

You just completed lesson 04 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.