Build CoreOrdered learning track

State Machines and Lifecycle Invariants

Learn Enterprise CPQ OMS Camunda 7 - Part 013

State machines and lifecycle invariants for quote and order flows in a production-grade Java microservices CPQ/OMS platform.

18 min read3464 words
PrevNext
Lesson 1364 lesson track1335 Build Core
#java#microservices#cpq#oms+8 more

Part 013 — State Machines and Lifecycle Invariants

CPQ/OMS yang production-grade tidak bisa hanya memakai kolom status.

Kolom status hanya menyimpan nama keadaan. State machine menjelaskan:

  1. state apa saja yang valid,
  2. transisi apa saja yang legal,
  3. siapa atau apa yang boleh memicu transisi,
  4. precondition apa yang harus benar sebelum transisi,
  5. side effect apa yang boleh terjadi setelah transisi,
  6. event apa yang harus diterbitkan,
  7. audit evidence apa yang harus ditinggalkan,
  8. transisi mana yang terminal dan tidak boleh dibalik,
  9. kegagalan mana yang bisa di-retry, di-compensate, atau harus masuk fallout.

Dalam CPQ/OMS, state machine bukan hiasan arsitektur. Ia adalah tulang punggung correctness.

Sistem yang buruk biasanya punya status seperti ini:

DRAFT -> SUBMITTED -> APPROVED -> ORDERED

Kelihatannya sederhana. Tapi begitu production berjalan, pertanyaannya muncul:

  • Apakah quote yang sudah APPROVED boleh diedit?
  • Apakah perubahan kecil pada contact information membatalkan approval?
  • Apakah perubahan discount membatalkan approval?
  • Apakah quote yang expired masih boleh accepted?
  • Apakah order boleh dibuat dua kali dari quote yang sama?
  • Apakah order line bisa COMPLETED saat parent order masih VALIDATING?
  • Apakah cancel order bisa dilakukan saat ada fulfillment step yang sudah completed?
  • Apakah retry dari Kafka consumer boleh mengubah state dua kali?
  • Apakah Camunda process boleh memajukan state entity langsung?
  • Apakah DB constraint bisa menangkap sebagian kesalahan lifecycle?

State machine menjawab pertanyaan itu secara eksplisit.

Top 1% engineer tidak memulai dari framework. Ia memulai dari transition legality dan domain invariants.


1. Status Bukan Lifecycle

status adalah data.

Lifecycle adalah aturan.

State machine adalah model yang membuat lifecycle bisa dieksekusi, diuji, diaudit, dan dipertahankan saat sistem tumbuh.

Perbedaan sederhananya:

KonsepPertanyaan yang Dijawab
StatusSekarang entity ada di keadaan apa?
LifecycleBagaimana entity boleh bergerak dari satu keadaan ke keadaan lain?
State machineApa aturan formal untuk setiap perpindahan keadaan?
InvariantKebenaran apa yang tidak boleh dilanggar oleh state apa pun?
CommandPermintaan eksplisit untuk mengubah lifecycle.
EventFakta bahwa lifecycle sudah berubah.
WorkflowOrkestrasi pekerjaan lintas service/manusia untuk mencapai state tertentu.

Kesalahan umum adalah membuat workflow engine menjadi pemilik state domain.

Itu berbahaya.

Camunda 7 boleh mengorkestrasi proses, assign task, menunggu callback, retry job, dan mencatat process history. Tapi state quote/order tetap harus dimiliki domain service masing-masing.

Aturan dasarnya:

Process state coordinates work. Domain state defines truth.

Kalau Camunda process instance mengatakan approval selesai, tapi quote service belum berhasil commit APPROVED, maka quote belum approved.

Kalau order process sudah sampai service task Notify Customer, tapi order aggregate masih VALIDATING, maka ada inconsistency yang harus dideteksi.


2. Vocabulary yang Harus Konsisten

Sebelum desain lifecycle, kita butuh vocabulary yang ketat.

2.1 State

State adalah kondisi stabil entity.

Contoh quote state:

DRAFT
CONFIGURED
PRICED
APPROVAL_REQUIRED
APPROVAL_IN_PROGRESS
APPROVED
REJECTED
ACCEPTED
CONVERTED_TO_ORDER
EXPIRED
CANCELLED

Contoh order state:

RECEIVED
VALIDATING
REJECTED
DECOMPOSING
READY_FOR_FULFILLMENT
FULFILLING
PARTIALLY_FULFILLED
COMPLETED
CANCELLATION_REQUESTED
CANCELLING
CANCELLED
FALLOUT

State harus cukup kaya untuk operation, tapi tidak terlalu granular sampai menjadi log step teknis.

State seperti CALLING_INVENTORY_API biasanya bukan domain state. Itu process/activity state.

State seperti FULFILLING bisa domain state karena punya makna bisnis.

2.2 Command

Command adalah permintaan perubahan.

Command diberi nama dengan imperative verb:

ConfigureQuote
PriceQuote
SubmitQuoteForApproval
ApproveQuote
RejectQuote
AcceptQuote
ConvertQuoteToOrder
CancelOrder
RetryFulfillmentStep
MarkOrderLineCompleted

Command bukan event. Command bisa ditolak.

Event adalah fakta yang sudah terjadi:

QuoteConfigured
QuotePriced
QuoteApprovalRequested
QuoteApproved
QuoteRejected
QuoteAccepted
OrderCreated
OrderValidationFailed
OrderFulfillmentStarted
OrderCompleted
OrderMovedToFallout

Event tidak boleh dinamai seperti perintah.

2.3 Guard

Guard adalah predicate yang harus benar agar transisi legal.

Contoh:

ApproveQuote guard:
- quote.status == APPROVAL_IN_PROGRESS
- currentUser has approvalAuthority for quote.requiredApprovalLevel
- quote.priceResultId == quote.currentPriceResultId
- quote.approvalRequestRevision == quote.currentRevision
- quote.expiredAt > now

Kalau guard gagal, sistem tidak boleh “mencoba tetap jalan”. Sistem harus menolak command secara eksplisit.

2.4 Invariant

Invariant adalah kebenaran yang harus tetap benar setelah setiap command.

Contoh:

A quote revision that has been accepted must never be mutated.

Atau:

An order must reference exactly one accepted quote revision, unless it is a non-quote order type explicitly allowed by policy.

Invariant lebih kuat daripada validasi request. Validasi request hanya memeriksa input. Invariant memeriksa kebenaran domain setelah operasi.

2.5 Effect

Effect adalah perubahan yang terjadi akibat command.

Effect bisa berupa:

  • perubahan row database,
  • insertion audit record,
  • insertion outbox event,
  • creation workflow instance,
  • correlation ke process instance,
  • invalidation cache,
  • scheduled expiration,
  • notification request.

Effect tidak boleh tersebar liar. Semua effect utama harus bisa dilacak dari command handler.

2.6 Terminal State

Terminal state adalah state yang tidak punya transisi keluar kecuali operation/admin recovery yang sangat terbatas.

Contoh quote terminal state:

CONVERTED_TO_ORDER
EXPIRED
CANCELLED

Contoh order terminal state:

COMPLETED
CANCELLED
REJECTED

FALLOUT tidak selalu terminal. Ia bisa menjadi holding state untuk manual recovery.


3. Quote State Machine

Quote lifecycle harus melindungi commercial evidence.

Quote bukan keranjang belanja. Quote adalah proposal komersial yang harus bisa dijelaskan ulang setelah diterima customer.

Diagram dasar:

Diagram ini bukan final untuk semua industri. Tetapi ia memberi shape yang benar:

  • quote bisa bergerak dari draft menuju configured,
  • quote harus diprice sebelum approval/acceptance,
  • approval bisa auto atau manual,
  • rejected quote tidak langsung approved lagi; harus direvisi,
  • accepted quote baru bisa converted to order,
  • expired/cancelled/converted adalah terminal untuk revision tersebut.

3.1 Quote State Semantics

StateMeaningMutabilityMain Risk
DRAFTQuote baru dibuat, belum punya konfigurasi valid.HighDraft dipakai untuk pricing/order.
CONFIGUREDProduct selection valid terhadap catalog snapshot.MediumCatalog drift tidak terlihat.
PRICEDQuote punya price result reproducible.Low/MediumPrice berubah tanpa trace.
APPROVAL_REQUIREDPrice/config butuh approval policy.LowApproval matrix salah atau stale.
APPROVAL_IN_PROGRESSHuman/system approval sedang berjalan.Very lowQuote diedit saat approval berjalan.
APPROVEDCommercial terms disetujui.Frozen commercial fieldsApproval tidak lagi valid setelah perubahan.
REJECTEDApproval ditolak.Revision requiredRe-submit tanpa perubahan yang jelas.
ACCEPTEDCustomer menerima quote revision.ImmutableMutasi setelah acceptance.
CONVERTED_TO_ORDERQuote revision sudah menghasilkan order.ImmutableDouble order creation.
EXPIREDValidity window habis.Immutable by defaultExpired quote accepted.
CANCELLEDQuote dihentikan sebelum accepted.Immutable by defaultCancelled quote resurrected tanpa audit.

3.2 Quote Transition Matrix

Transition matrix lebih berguna daripada diagram saat implementasi.

FromCommandToGuard UtamaEffect Utama
DRAFTConfigureQuoteCONFIGUREDproduct offering valid; config rules satisfiedsave configuration snapshot; emit QuoteConfigured
CONFIGUREDPriceQuotePRICEDconfiguration valid; price book availablesave price result; emit QuotePriced
PRICEDDetectApprovalRequiredAPPROVAL_REQUIREDapproval policy says manual approval neededsave approval requirement; start process optional
PRICEDAutoApproveAPPROVEDapproval policy says no approval neededsave approval evidence; emit QuoteApproved
APPROVAL_REQUIREDSubmitForApprovalAPPROVAL_IN_PROGRESSnot expired; no active approval processstart Camunda process; save process correlation
APPROVAL_IN_PROGRESSApproveQuoteAPPROVEDapprover authorized; revision unchangedsave approval decision; emit QuoteApproved
APPROVAL_IN_PROGRESSRejectQuoteREJECTEDapprover authorizedsave rejection reason; emit QuoteRejected
REJECTEDReviseQuoteCONFIGUREDuser can revise; new revision createdclone allowed fields; create new revision
APPROVEDAcceptQuoteACCEPTEDcustomer acceptance valid; not expiredsave acceptance evidence; emit QuoteAccepted
ACCEPTEDConvertQuoteToOrderCONVERTED_TO_ORDERno existing order for quote revisioncreate order command/outbox; lock conversion

Transition matrix adalah artifact engineering. Ia harus hidup dekat dengan code, test, dan documentation.


4. Order State Machine

Order lifecycle lebih keras karena order menyentuh fulfillment, external systems, inventory, billing, dan customer commitment.

Order state harus membedakan business rejection dan technical fallout.

REJECTED berarti order tidak valid secara bisnis.

FALLOUT berarti proses gagal atau ambigu dan butuh recovery.

Mencampur keduanya akan merusak reporting dan operasi.

4.1 Order State Semantics

StateMeaningOwner UtamaMain Risk
RECEIVEDOrder sudah dibuat dari accepted quote/command.Order serviceDuplicate creation.
VALIDATINGBusiness validation sedang berjalan.Order service + workflowTimeout dianggap valid.
REJECTEDOrder tidak bisa diproses karena invalid.Order serviceTechnical error disalahartikan sebagai rejection.
DECOMPOSINGOrder line dipecah ke fulfillment tasks.Order serviceDecomposition tidak deterministic.
READY_FOR_FULFILLMENTSemua fulfillment plan siap.Order servicePlan stale sebelum dieksekusi.
FULFILLINGEksekusi external/internal system berjalan.Workflow + fulfillment adaptersUnknown outcome.
PARTIALLY_FULFILLEDSebagian obligation sudah selesai.Order serviceCancel semantics ambigu.
COMPLETEDSemua obligation selesai.Order serviceCompleted tanpa evidence lengkap.
CANCELLATION_REQUESTEDCancel intent diterima.Order serviceCancel dianggap langsung sukses.
CANCELLINGCompensation/reversal berjalan.Workflow + adaptersPartial cancellation tidak dimodelkan.
CANCELLEDCancellation selesai sesuai policy.Order serviceHilang trace dari fulfilled lines.
FALLOUTPerlu manual/automated recovery.OperationsFallout jadi kuburan permanen.

5. Lifecycle Invariant Catalog

Invariant catalog adalah daftar kebenaran yang harus dipertahankan oleh code, database, test, dan operation.

Jangan simpan invariant hanya di kepala engineer senior.

Tulis. Uji. Review. Jadikan bagian dari architecture governance.

5.1 Identity and Version Invariants

QI-001: Every quote has a stable quoteId.
QI-002: Every accepted commercial state belongs to a specific quoteRevisionId.
QI-003: A quote revision number is monotonically increasing within a quote.
QI-004: Only one current revision exists for an active quote at any point in time.
QI-005: Immutable revisions must not be updated in place.

Implementation hint:

  • quote_id adalah identity bisnis utama.
  • quote_revision_id adalah identity evidence.
  • revision_number adalah urutan manusiawi.
  • current_revision_id boleh disimpan di parent quote, tapi harus konsisten.

5.2 Configuration Invariants

CI-001: A priced quote must reference exactly one configuration snapshot.
CI-002: A configuration snapshot must identify catalog version used for validation.
CI-003: A quote cannot be priced if configuration is invalid.
CI-004: A configuration change after pricing invalidates price result.
CI-005: A configuration change after approval invalidates approval unless policy explicitly says otherwise.

Yang penting bukan hanya “config valid sekarang”.

Yang penting adalah:

valid terhadap catalog version apa?

Tanpa itu, quote lama tidak bisa dijelaskan saat catalog berubah.

5.3 Pricing Invariants

PI-001: An accepted quote must have exactly one final price result.
PI-002: A price result must be reproducible from input snapshot, price book version, rule version, and rounding policy.
PI-003: Manual price override must include actor, reason, timestamp, and authority boundary.
PI-004: A price result used for approval must be the same price result accepted by customer.
PI-005: Currency and rounding policy must be explicit at quote revision level.

Pricing error biasanya tidak terlihat saat create quote.

Ia terlihat ketika dispute, billing mismatch, atau audit.

5.4 Approval Invariants

AI-001: Approval decision must reference quote revision and price result.
AI-002: Approval authority must be evaluated at decision time.
AI-003: Approval must not be reused across commercial changes.
AI-004: Approval rejection must include reason category.
AI-005: Approval process must have correlation to process instance when workflow-managed.

Approval bukan boolean approved=true.

Approval adalah evidence.

5.5 Acceptance Invariants

ACI-001: Customer acceptance must reference exact quote revision.
ACI-002: Accepted quote revision must be immutable.
ACI-003: Accepted quote must not be expired at acceptance time.
ACI-004: Acceptance must record channel, actor, timestamp, and terms version.
ACI-005: Accepted quote can produce at most one primary order unless amendment policy allows derived orders.

Acceptance adalah legal/commercial boundary.

Setelah acceptance, editing quote berarti merusak bukti.

Kalau perlu perubahan, buat amendment/change quote.

5.6 Order Invariants

OI-001: Order created from quote must reference accepted quote revision.
OI-002: Order line must trace to quote line unless explicitly created as system-generated line.
OI-003: Parent order cannot be COMPLETED while any mandatory order line is not terminal-completed.
OI-004: Fulfillment step must be idempotent by external correlation key.
OI-005: External system unknown outcome must not be converted into success without reconciliation evidence.
OI-006: Cancellation must preserve fulfilled obligation trace.

OMS yang matang tidak hanya tahu order sukses.

Ia tahu bagian mana yang sukses, bagian mana yang gagal, bagian mana yang ambiguous, dan bagian mana yang perlu manual action.

5.7 Event Invariants

EI-001: Every committed lifecycle transition must produce at most one corresponding domain event per command id.
EI-002: Event payload must reference aggregate id, aggregate version, event type, event version, occurredAt, and correlation id.
EI-003: Event publication must not happen outside database transaction unless outbox/reconciliation guarantees exist.
EI-004: Consumers must tolerate duplicate events.
EI-005: Replay must not recreate non-idempotent side effects.

Kafka tidak memperbaiki domain model yang ambigu.

Kafka hanya menyebarkan ambiguity lebih cepat.

5.8 Workflow Invariants

WI-001: A process instance must correlate to a business key.
WI-002: Workflow variables must not be the only source of domain truth.
WI-003: Human task completion must call domain command, not mutate entity state directly.
WI-004: Process retry must not duplicate domain side effects.
WI-005: Workflow incident must be linkable to quote/order id and correlation id.

BPMN boleh kompleks. Domain command harus tetap tegas.


6. Command Handler Shape

State machine harus terlihat di command handler.

Contoh shape konseptual:

public final class ApproveQuoteHandler {

    public ApproveQuoteResult handle(ApproveQuoteCommand command) {
        IdempotencyDecision decision = idempotencyStore.begin(
            command.commandId(),
            command.idempotencyKey()
        );

        if (decision.isReplay()) {
            return decision.previousResultAs(ApproveQuoteResult.class);
        }

        QuoteRevision quote = quoteRepository.getForUpdate(
            command.quoteRevisionId()
        );

        quote.assertVersion(command.expectedVersion());
        quote.assertState(QuoteState.APPROVAL_IN_PROGRESS);
        quote.assertNotExpired(clock.now());
        quote.assertPriceResultStillCurrent();
        approvalPolicy.assertApproverCanApprove(
            command.actor(),
            quote.requiredApprovalLevel(),
            quote.commercialContext()
        );

        quote.approve(command.actor(), command.reason(), clock.now());

        quoteRepository.save(quote);
        outbox.append(QuoteApprovedEvent.from(quote, command));
        audit.append(AuditRecord.quoteApproved(quote, command));

        return idempotencyStore.commit(
            command.commandId(),
            ApproveQuoteResult.from(quote)
        );
    }
}

Perhatikan urutannya:

  1. idempotency boundary,
  2. load aggregate dengan concurrency control,
  3. expected version check,
  4. state guard,
  5. domain guard,
  6. policy guard,
  7. state mutation,
  8. outbox event,
  9. audit record,
  10. idempotency commit.

Ini bukan template mekanis. Ini urutan perlindungan.

Kalau urutan ini kacau, production bug akan muncul sebagai duplicate order, stale approval, phantom success, atau incident yang tidak bisa dijelaskan.


7. Guard Layering

Tidak semua guard berada di tempat yang sama.

Guard TypeLokasiContoh
Syntactic guardAPI/request validationrequired field, enum valid, date format
Authorization guardsecurity/policy layeruser boleh approve level tertentu
State guardaggregate/domain servicequote harus APPROVAL_IN_PROGRESS
Business guarddomain serviceprice masih current, quote belum expired
Persistence guarddatabase constraintunique order per quote revision
Integration guardadapter/clientexternal correlation id exists
Workflow guardprocess orchestrationtask belongs to process/business key

Kesalahan umum adalah menaruh semua guard di API layer.

API layer hanya pintu masuk. Invariant tetap harus hidup di domain dan database.


8. Expected Version dan Optimistic Concurrency

Dalam CPQ/OMS, user sering bekerja paralel:

  • sales rep edit quote,
  • pricing analyst apply override,
  • approver approve quote,
  • customer accept quote via portal,
  • scheduled job expire quote,
  • integration callback masuk dari external system.

Tanpa concurrency model, lifecycle akan race.

Command yang mengubah aggregate harus membawa expected version:

{
  "commandId": "cmd-9e1f",
  "quoteRevisionId": "qr-2026-000012-r3",
  "expectedVersion": 17,
  "decision": "APPROVE",
  "reason": "Discount within delegated authority"
}

Kalau current version sudah 18, command harus ditolak dengan conflict.

Jangan diam-diam merge lifecycle command.

State transition bukan dokumen kolaboratif.

8.1 Conflict Bukan Error Teknis

HTTP response untuk stale expected version harus diperlakukan sebagai business conflict.

Contoh semantic:

{
  "type": "https://errors.example.com/conflict/stale-aggregate-version",
  "title": "Quote revision has changed",
  "status": 409,
  "detail": "The command expected version 17 but current version is 18.",
  "aggregateId": "qr-2026-000012-r3",
  "expectedVersion": 17,
  "currentVersion": 18,
  "correlationId": "cor-01H..."
}

Client harus refresh, bukan retry buta.


9. Idempotency untuk Lifecycle Command

Idempotency bukan hanya untuk payment.

Dalam CPQ/OMS, idempotency dibutuhkan untuk:

  • AcceptQuote,
  • ConvertQuoteToOrder,
  • SubmitOrder,
  • StartFulfillment,
  • CancelOrder,
  • external callback,
  • Kafka consumer command handler,
  • Camunda external task worker.

Command idempotency menjawab:

Jika command yang sama masuk lagi, apakah sistem bisa mengembalikan hasil yang sama tanpa side effect baru?

9.1 Command ID vs Idempotency Key

Gunakan dua konsep:

FieldMeaning
commandIdunique identity command invocation.
idempotencyKeyclient/business key untuk deduplication semantic.

Contoh:

idempotencyKey = quoteRevisionId + ":convert-to-order"

Untuk conversion, satu accepted quote revision hanya boleh menghasilkan satu primary order.

Database juga harus membantu:

CREATE UNIQUE INDEX uq_order_source_quote_revision
ON oms_order(source_quote_revision_id)
WHERE source_quote_revision_id IS NOT NULL
  AND order_type = 'PRIMARY';

Application idempotency mencegah duplicate side effect.

Database uniqueness mencegah race yang lolos dari application layer.

Keduanya dibutuhkan.


10. Lifecycle and Camunda 7 Boundary

Camunda 7 sangat berguna untuk approval dan order orchestration. Tetapi workflow engine tidak boleh menjadi tempat tersembunyi untuk aturan domain.

10.1 Quote Approval Boundary

BPMN approval process boleh berisi:

  • create approval task,
  • route to approval group,
  • wait for decision,
  • escalate SLA,
  • notify requester,
  • call quote service command.

Namun approval decision yang sah tetap harus lewat command:

ApproveQuote
RejectQuote
RequestQuoteRevision

Bukan update DB quote langsung dari task listener.

10.2 Order Orchestration Boundary

BPMN order process boleh berisi:

  • validate order,
  • reserve inventory,
  • create fulfillment request,
  • wait callback,
  • retry technical failures,
  • execute compensation,
  • create manual fallout task.

Tetapi state order harus berubah lewat order service command:

MarkOrderValidationStarted
MarkOrderValid
MarkOrderRejected
StartFulfillment
MarkFulfillmentStepCompleted
MoveOrderToFallout
CompleteOrder

Camunda process adalah orchestrator.

Order service adalah source of truth.

10.3 Process Correlation

Minimal correlation fields:

businessKey = orderId or quoteRevisionId
processInstanceId
processDefinitionKey
processDefinitionVersion
rootCorrelationId
lastDomainEventId

Correlation table akan dibahas di Part 014.


11. Lifecycle State vs Read Model State

Jangan campur state domain dengan state tampilan.

Contoh domain state:

APPROVAL_IN_PROGRESS

Contoh UI/read model derived state:

Waiting for Finance Approval
Waiting for Director Approval
Approval SLA Breached
Approval Task Unassigned

Derived state boleh berubah tanpa mengubah aggregate lifecycle.

Kalau semua variasi UI dimasukkan ke domain status, status list akan meledak dan sulit dijaga.

Rule:

Domain state should model business truth. Read model state should model user attention.


12. State Machine as Code

State machine harus ada dalam code, bukan hanya diagram.

Minimal approach:

public enum QuoteState {
    DRAFT,
    CONFIGURED,
    PRICED,
    APPROVAL_REQUIRED,
    APPROVAL_IN_PROGRESS,
    APPROVED,
    REJECTED,
    ACCEPTED,
    CONVERTED_TO_ORDER,
    EXPIRED,
    CANCELLED
}

Lalu transition policy:

public final class QuoteTransitionPolicy {

    private static final Map<QuoteState, Set<QuoteCommandType>> ALLOWED = Map.of(
        QuoteState.DRAFT, Set.of(CONFIGURE_QUOTE, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.CONFIGURED, Set.of(UPDATE_CONFIGURATION, PRICE_QUOTE, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.PRICED, Set.of(DETECT_APPROVAL_REQUIRED, AUTO_APPROVE, CHANGE_CONFIGURATION, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.APPROVAL_REQUIRED, Set.of(SUBMIT_FOR_APPROVAL, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.APPROVAL_IN_PROGRESS, Set.of(APPROVE_QUOTE, REJECT_QUOTE, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.REJECTED, Set.of(REVISE_QUOTE),
        QuoteState.APPROVED, Set.of(ACCEPT_QUOTE, CANCEL_QUOTE, EXPIRE_QUOTE),
        QuoteState.ACCEPTED, Set.of(CONVERT_TO_ORDER),
        QuoteState.CONVERTED_TO_ORDER, Set.of(),
        QuoteState.EXPIRED, Set.of(),
        QuoteState.CANCELLED, Set.of()
    );

    public void assertAllowed(QuoteState state, QuoteCommandType commandType) {
        if (!ALLOWED.getOrDefault(state, Set.of()).contains(commandType)) {
            throw new IllegalLifecycleTransitionException(state, commandType);
        }
    }
}

Tetapi jangan berhenti di table allowed command.

Allowed transition hanya menjawab “boleh dari state ini?”.

Guard menjawab “boleh dalam kondisi ini?”.


13. Transition Failure Taxonomy

Tidak semua failure sama.

FailureMeaningResponse
Invalid command shapeRequest tidak valid.400 validation error
Unauthorized actorActor tidak boleh melakukan command.403 authorization error
Illegal transitionState sekarang tidak menerima command.409 lifecycle conflict
Stale versionAggregate berubah sejak client membaca.409 stale version conflict
Business guard failedSyarat bisnis tidak terpenuhi.422 business rule violation atau 409 tergantung semantic
Technical dependency failedDependency error/timeout.retry atau 503
Unknown external outcomeTidak tahu external system berhasil/gagal.reconciliation/fallout, bukan sukses/gagal asal
Duplicate commandCommand sudah diproses.return previous result
Duplicate semantic actionAction sudah terjadi via command lain.409 atau return existing resource sesuai policy

Failure taxonomy mempengaruhi retry.

Retry illegal transition tidak berguna.

Retry timeout mungkin berguna.

Retry unknown external outcome tanpa reconciliation bisa menciptakan duplikasi.


14. Database Enforcement for State Machine

Database tidak bisa mengekspresikan semua lifecycle rule dengan mudah. Tapi database harus menjaga invariant yang murah dan kritikal.

Contoh:

ALTER TABLE quote_revision
ADD CONSTRAINT chk_quote_revision_status
CHECK (status IN (
  'DRAFT',
  'CONFIGURED',
  'PRICED',
  'APPROVAL_REQUIRED',
  'APPROVAL_IN_PROGRESS',
  'APPROVED',
  'REJECTED',
  'ACCEPTED',
  'CONVERTED_TO_ORDER',
  'EXPIRED',
  'CANCELLED'
));

Contoh accepted quote harus punya accepted timestamp:

ALTER TABLE quote_revision
ADD CONSTRAINT chk_quote_acceptance_timestamp
CHECK (
  (status = 'ACCEPTED' AND accepted_at IS NOT NULL)
  OR
  (status <> 'ACCEPTED')
);

Contoh satu primary order per accepted quote revision:

CREATE UNIQUE INDEX uq_primary_order_per_quote_revision
ON oms_order(source_quote_revision_id)
WHERE source_quote_revision_id IS NOT NULL
  AND order_type = 'PRIMARY';

Database bukan pengganti domain model.

Database adalah safety net terakhir.


15. Invariant-Driven Testing

Test lifecycle tidak cukup dengan happy path.

Gunakan invariant-driven test matrix.

15.1 Quote Lifecycle Tests

TestGivenWhenThen
Price requires valid configDRAFT quotePriceQuoterejected illegal transition
Approval locks revisionAPPROVAL_IN_PROGRESSupdate price-sensitive configrejected or new revision required
Approved quote can expireAPPROVED, validUntil < nowscheduled expirystate EXPIRED
Expired quote cannot be acceptedEXPIREDAcceptQuotelifecycle conflict
Accepted quote immutableACCEPTEDupdate discountrejected
Conversion idempotentACCEPTED, same idempotency key twiceConvertQuoteToOrder twiceone order only

15.2 Order Lifecycle Tests

TestGivenWhenThen
Invalid order rejectedVALIDATING order with failed business validationRejectInvalidOrderstate REJECTED with reason
Technical failure goes falloutinventory timeout ambiguityworkflow reports unknownstate FALLOUT
Completed requires all lines terminalsome mandatory line openCompleteOrderrejected
Cancel fulfilled order needs compensationpartially fulfilled orderRequestCancellationstate CANCELLATION_REQUESTED then compensation path
Duplicate callback safeexternal callback delivered twiceprocess callback twiceone state change only
Recovery auditableorder in FALLOUTmanual recoveryaudit record + controlled transition

15.3 Property-Like Lifecycle Test

Untuk lifecycle kritikal, test matrix bisa dibuat lebih generatif:

For every state S
For every command C not allowed in S
When C is executed against aggregate in S
Then command is rejected
And aggregate version does not change
And no outbox event is appended
And no audit success record is appended

Ini sederhana, tapi menangkap banyak bug.


16. Transition Logs vs Audit Logs

Jangan mengira updated_at cukup.

Lifecycle transition harus punya transition log:

transition_id
aggregate_type
aggregate_id
from_state
to_state
command_type
command_id
actor_type
actor_id
reason_code
reason_text
correlation_id
occurred_at

Audit log lebih luas. Ia menjawab siapa melakukan apa.

Transition log lebih spesifik. Ia menjawab bagaimana state bergerak.

Dalam regulated/commercial system, keduanya sering dibutuhkan.


17. Operational Smells

State machine buruk biasanya terlihat dari production symptoms berikut:

SymptomKemungkinan Akar Masalah
Banyak row dengan status PROCESSING permanenTidak ada fallout/recovery state yang jelas.
Banyak boolean seperti isApproved, isAccepted, isSubmittedState model tidak eksplisit.
Order berhasil tapi quote masih acceptedConversion boundary tidak atomic/idempotent.
Approver approve quote yang sudah berubahApproval tidak mengikat revision/price result.
Customer accept quote expiredExpiry guard tidak ada di acceptance command.
Retry membuat dua fulfillment requestExternal correlation/idempotency lemah.
Manual DB update sering dilakukanRecovery command/admin tool tidak dirancang.
UI punya status sendiri yang beda dari backendRead model dan domain state tidak jelas.

Operational smell adalah feedback desain.

Jangan hanya patch data. Perbaiki lifecycle model.


18. Minimal Lifecycle Governance Artifact

Untuk setiap aggregate penting, buat file governance seperti ini:

Aggregate: QuoteRevision
Owner Service: quote-service
Identity: quoteRevisionId
Version Field: version
Terminal States: CONVERTED_TO_ORDER, EXPIRED, CANCELLED
Primary Commands:
  - ConfigureQuote
  - PriceQuote
  - SubmitForApproval
  - ApproveQuote
  - RejectQuote
  - AcceptQuote
  - ConvertQuoteToOrder
Events:
  - QuoteConfigured
  - QuotePriced
  - QuoteApprovalRequested
  - QuoteApproved
  - QuoteRejected
  - QuoteAccepted
  - QuoteConvertedToOrder
Invariant Catalog:
  - QI-001 ...
  - PI-001 ...
  - AI-001 ...
Database Constraints:
  - chk_quote_revision_status
  - uq_quote_current_revision
  - uq_primary_order_per_quote_revision
Workflow Correlations:
  - quoteApprovalProcess by quoteRevisionId

Ini bukan birokrasi.

Ini adalah cara membuat lifecycle bisa direview oleh engineer baru, architect, QA, ops, dan compliance.


19. Design Rule: State Transition is a Transaction Boundary

Untuk command lifecycle utama, perlakukan transisi state sebagai transaction boundary.

Dalam satu database transaction, idealnya terjadi:

  1. aggregate loaded and locked/checked,
  2. guards evaluated,
  3. state changed,
  4. aggregate version incremented,
  5. transition log inserted,
  6. audit record inserted,
  7. outbox event inserted,
  8. idempotency result stored.

Hal yang tidak boleh ada di tengah transaction:

  • call external API lambat,
  • publish langsung ke Kafka tanpa outbox,
  • call email service,
  • call document generator,
  • start long-running workflow yang tidak bisa direconcile.

External side effect harus keluar melalui outbox/workflow setelah commit, atau punya reconciliation boundary yang jelas.


20. Final Mental Model

State machine menjadikan CPQ/OMS bisa dipercaya.

Tanpa state machine, sistem hanya punya kumpulan endpoint yang kebetulan mengubah status.

Dengan state machine, sistem punya grammar:

Command + Current State + Guards -> New State + Events + Audit + Effects

Itulah inti lifecycle correctness.

Quote lifecycle melindungi commercial evidence.

Order lifecycle melindungi fulfillment obligation.

Camunda 7 mengorkestrasi pekerjaan.

PostgreSQL menjaga kebenaran yang harus tidak bisa dilanggar.

Kafka menyebarkan fakta setelah commit.

Redis membantu ephemeral speed, bukan sumber kebenaran lifecycle.

Kalau satu prinsip harus diingat dari part ini:

Jangan pernah membiarkan state berubah hanya karena sebuah endpoint dipanggil. State hanya boleh berubah karena command legal melewati guard dan meninggalkan evidence.


21. Checklist Part 013

Gunakan checklist ini sebelum lanjut ke data model:

  • Setiap aggregate penting punya state list eksplisit.
  • Setiap state punya meaning dan mutability policy.
  • Setiap command punya allowed source state.
  • Setiap command punya guard utama.
  • Setiap transition punya event dan audit record.
  • Terminal state jelas.
  • Fallout state dibedakan dari business rejection.
  • Expected version dipakai untuk command mutasi.
  • Idempotency key dipakai untuk command yang rawan duplikasi.
  • Camunda process tidak menjadi source of truth domain state.
  • DB constraint menjaga invariant murah dan kritikal.
  • Lifecycle test mencakup illegal transitions.
  • Recovery path tidak bergantung pada manual SQL update.

Part berikutnya menerjemahkan invariant ini ke PostgreSQL data model: table, constraint, index, audit, outbox, idempotency, dan correlation table.

Lesson Recap

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

Continue The Track

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