Build CoreOrdered learning track

ERP State Machines and Lifecycle Modelling

Learn Java Large Scale ERP - Part 018

Deep dive into ERP state machines and lifecycle modelling for large-scale Java ERP, including legal transitions, guards, actions, transition logs, document revision, cancellation, reversal, closure, migration, concurrency, and auditability.

15 min read2863 words
PrevNext
Lesson 1834 lesson track0718 Build Core
#java#erp#state-machine#lifecycle-modelling+4 more

Part 018 — ERP State Machines and Lifecycle Modelling

1. Target Skill Part Ini

ERP besar hidup dari lifecycle dokumen: requisition, purchase order, goods receipt, invoice, sales order, shipment, payment, stock adjustment, work order, journal, asset, project, service order, dan case. Hampir semua bug serius ERP muncul ketika lifecycle tidak dimodelkan dengan tegas.

Skill inti part ini: mampu mendesain state machine ERP yang explicit, auditable, concurrency-safe, version-aware, dan menjaga business invariant pada setiap transisi dokumen.

Kesalahan umum:

if (status.equals("APPROVED")) {
    status = "POSTED";
}

Masalahnya bukan syntax. Masalahnya adalah APPROVED tidak cukup menjelaskan:

  • apakah dokumen masih revision yang sama;
  • apakah approval masih valid;
  • apakah period terbuka;
  • apakah stock/budget/fund tersedia;
  • apakah external integration sudah selesai;
  • apakah ada hold/block;
  • apakah transition boleh dilakukan oleh actor tersebut;
  • apakah dokumen sudah pernah diposting;
  • apakah cancellation atau reversal harus dipakai;
  • apakah transition perlu menghasilkan accounting event, inventory movement, task, atau case.

State machine yang benar bukan enum status. Ia adalah kontrak lifecycle.


2. Kaufman Deconstruction: Memecah Skill State Machine ERP

Sub-skillPertanyaan yang Harus Bisa DijawabOutput Engineering
State semanticsApa arti setiap state secara bisnis?state glossary
Event modellingEvent apa yang memicu transition?command/event catalog
GuardSyarat apa yang harus benar sebelum transition?guard function, invariant check
ActionSide effect apa yang terjadi setelah transition?domain event, outbox, task
Transition matrixDari state mana ke state mana legal?transition table
RevisionTransition berlaku untuk versi dokumen apa?document revision model
AuditBukti transition apa yang disimpan?transition log
CancellationApa beda cancel, void, reject, close?terminal semantics
ReversalBagaimana membatalkan efek yang sudah posted?compensating transaction
ConcurrencyBagaimana dua transition bersamaan ditangani?optimistic/pessimistic lock
MigrationBagaimana lifecycle berubah saat upgrade?state migration plan
TestingBagaimana membuktikan illegal transition gagal?state machine test suite

Kita akan membangun state machine sebagai mental model yang bisa diterapkan pada semua domain ERP.


3. State Machine Bukan Cuma Enum

Enum hanya menyebut kemungkinan status.

public enum PurchaseOrderStatus {
    DRAFT,
    SUBMITTED,
    APPROVED,
    CANCELLED,
    CLOSED
}

State machine menjawab:

current state
+ event
+ actor/context
+ guard
+ action
+ next state
+ evidence

Formal sederhana:

Transition = f(currentState, event, context) -> nextState + actions
only if guards(context) are true

Contoh:

DRAFT + SUBMIT
  guard: has at least one line, vendor active, total > 0, period open
  action: create approval workflow
  next: SUBMITTED

4. Empat Jenis Status yang Sering Tercampur

ERP sering rusak karena semua status ditaruh di satu field.

Status TypeMenjawabContoh
Business lifecycle statusdokumen berada pada tahap apa?DRAFT, APPROVED, POSTED, CLOSED
Approval statuskeputusan approval bagaimana?PENDING, APPROVED, REJECTED
Posting statusefek akuntansi/inventory sudah terjadi?NOT_POSTED, POSTED, REVERSED
Integration statussinkronisasi external bagaimana?PENDING_EXPORT, SENT, ACKED, FAILED

Contoh buruk:

status = SENT

Sent ke siapa? Sent invoice ke customer? Sent event ke broker? Sent payment file ke bank? Sent approval request ke manager?

Desain lebih baik:

document_state = APPROVED
approval_state = COMPLETED
posting_state = NOT_POSTED
integration_state = PENDING_EXPORT

Tetapi jangan juga membuat terlalu banyak status tanpa aturan. Kuncinya adalah setiap status punya ownership dan transition rule.


5. Canonical ERP Document Lifecycle

Banyak dokumen ERP mengikuti pola ini, dengan variasi domain.

Tidak semua domain memakai semua state. Tetapi lifecycle umum ini membantu melihat pola:

  • sebelum approval: bisa diedit lebih bebas;
  • setelah approval: edit harus dibatasi atau memicu reapproval;
  • setelah posting: tidak boleh diedit langsung, harus reversal/adjustment;
  • setelah close: hanya correction administratif yang sangat terbatas;
  • terminal state harus jelas.

6. State Glossary: Jangan Biarkan Nama State Ambigu

State harus punya definisi eksplisit.

StateMeaningEditable?Financial Effect?Typical Exit
Draftdokumen sedang dibuatyesnosubmit/cancel
Submitteddikirim untuk validasi/approvallimitednoreview/return/reject
InReviewtask approval aktiflimitednoapprove/reject/request change
Approvedapproved untuk revision tertentuusually nono or pendingpost/cancel/revise
Postedefek ledger/stock sudah dicatatno direct edityesreverse/close
PartiallyFulfilledsebagian execution terjadirestrictedyes/partialfulfill/reverse/close
Fulfilledexecution selesainoyesclose
Reversedefek posted dibalik oleh dokumen kompensasinoreversal effectclose
Cancelleddibatalkan sebelum irreversible effectnono or reversal neededterminal
Rejectedditolak oleh approvalno or copy-to-newnoterminal
Closedlifecycle selesainofinalterminal

State glossary harus menjawab:

  • siapa owner state ini;
  • apakah dokumen bisa diubah;
  • apakah approval masih valid;
  • apakah posting sudah terjadi;
  • apa action legal;
  • apa evidence minimal;
  • apakah state terminal;
  • bagaimana report harus memperlakukannya.

7. Transition Matrix

State diagram bagus untuk visualisasi, tetapi transition matrix lebih kuat untuk implementation dan test.

Contoh PO:

Current StateEventGuardNext StateAction
DRAFTSUBMITvalid lines, vendor activeSUBMITTEDcreate workflow
SUBMITTEDSTART_REVIEWworkflow definition existsIN_REVIEWcreate tasks
IN_REVIEWREQUEST_CHANGEactor can reviewRETURNEDclose task, notify requester
RETURNEDRESUBMITchanged revision validSUBMITTEDreroute workflow
IN_REVIEWAPPROVEall approvals completeAPPROVEDrecord approval snapshot
IN_REVIEWREJECTactor can rejectREJECTEDclose workflow
APPROVEDPOSTperiod open, budget okPOSTEDcreate accounting/inventory event
APPROVEDCANCELno downstream executionCANCELLEDcancel approval/workflow
POSTEDREVERSEreversal allowedREVERSEDcreate reversal document
POSTEDCLOSEfully fulfilledCLOSEDarchive/lock

Transition matrix bisa diturunkan menjadi unit test:

@ParameterizedTest
@MethodSource("illegalTransitions")
void illegalTransitionsMustFail(DocumentState current, DocumentEvent event) {
    var po = PurchaseOrderFixture.inState(current);
    assertThrows(IllegalTransitionException.class,
            () -> stateMachine.apply(po, event, context));
}

8. Guard Design

Guard adalah predicate yang menentukan apakah transition legal.

public interface TransitionGuard<D, C> {
    GuardDecision evaluate(D document, C context);
}

public record GuardDecision(
        boolean allowed,
        String code,
        List<String> reasons,
        Map<String, Object> evidence
) {
    public static GuardDecision allow(String code) {
        return new GuardDecision(true, code, List.of(), Map.of());
    }

    public static GuardDecision deny(String code, String reason) {
        return new GuardDecision(false, code, List.of(reason), Map.of());
    }
}

8.1 Guard Examples

public final class PurchaseOrderSubmitGuard
        implements TransitionGuard<PurchaseOrder, TransitionContext> {

    @Override
    public GuardDecision evaluate(PurchaseOrder po, TransitionContext ctx) {
        if (po.lines().isEmpty()) {
            return GuardDecision.deny("PO_SUBMIT", "PO must have at least one line");
        }
        if (!po.vendorSnapshot().active()) {
            return GuardDecision.deny("PO_SUBMIT", "Vendor is inactive");
        }
        if (po.total().isZeroOrNegative()) {
            return GuardDecision.deny("PO_SUBMIT", "PO total must be positive");
        }
        if (ctx.periodService().isLocked(po.documentDate(), po.legalEntityId())) {
            return GuardDecision.deny("PO_SUBMIT", "Accounting period is locked");
        }
        return GuardDecision.allow("PO_SUBMIT");
    }
}

Guard harus:

  • deterministic;
  • side-effect free;
  • menghasilkan reason code;
  • bisa dites;
  • tidak melakukan update database;
  • tidak memanggil external service volatile secara langsung;
  • menyimpan evidence jika dipakai untuk decision penting.

9. Actions: Side Effect Setelah Transition

Transition action adalah efek yang terjadi setelah state berpindah.

Contoh:

TransitionAction
DRAFT → SUBMITTEDcreate workflow instance
IN_REVIEW → APPROVEDstore approval snapshot
APPROVED → POSTEDcreate posting request/outbox event
POSTED → REVERSEDcreate reversal document
ANY → CANCELLEDcancel active tasks/timers
FULFILLED → CLOSEDlock document and archive read model

Action harus diproses aman:

state update
+ transition log
+ domain event
+ outbox message
commit together

External side effect setelah commit.

@Transactional
public TransitionResult transition(TransitionCommand command) {
    Document doc = repository.getForUpdate(command.documentId());
    TransitionPlan plan = stateMachine.plan(doc, command.event(), command.context());

    plan.assertAllowed();
    doc.apply(plan);

    repository.save(doc);
    transitionLogRepository.append(TransitionLog.from(plan));
    outboxRepository.saveAll(plan.outboxEvents());

    return TransitionResult.success(doc.id(), doc.state(), doc.revision());
}

10. Persistent State Machine Model

State machine harus persistent dan auditable.

10.1 Document Table

create table erp_document (
    id uuid primary key,
    document_type varchar(80) not null,
    document_number varchar(120) not null,
    legal_entity_id uuid not null,
    branch_id uuid,
    document_state varchar(60) not null,
    approval_state varchar(60) not null,
    posting_state varchar(60) not null,
    integration_state varchar(60) not null,
    revision integer not null,
    version integer not null,
    created_at timestamptz not null,
    updated_at timestamptz not null,
    unique (document_type, document_number)
);

10.2 Transition Log

create table document_transition_log (
    id uuid primary key,
    document_id uuid not null,
    document_type varchar(80) not null,
    from_state varchar(60) not null,
    to_state varchar(60) not null,
    event varchar(80) not null,
    actor_id uuid,
    transition_time timestamptz not null,
    revision integer not null,
    command_id varchar(160) not null,
    guard_evidence jsonb not null,
    action_evidence jsonb not null,
    reason_code varchar(120),
    comment text,
    hash varchar(128) not null,
    previous_hash varchar(128),
    unique (document_id, command_id)
);

Transition log menjawab:

who moved this document from state A to state B,
when,
why,
under which guard decision,
for which revision,
with what command id.

11. Document Revision and State

Revision harus dipisah dari version.

FieldMeaningDipakai Untuk
revisionbusiness content revisionapproval binding, audit
versionpersistence concurrency versionoptimistic locking

Contoh:

revision = 4 means business content revision 4
version = 23 means row has been updated 23 times

Perubahan komentar internal mungkin menaikkan version, tetapi tidak selalu menaikkan revision. Perubahan amount/vendor/line harus menaikkan revision.

Revision Policy

ChangeRevision?Reapproval?
edit line quantityyesyes
edit internal noteno/maybeno
attach contractyesyes
add non-approval attachmentmaybeno
change cost centeryesyes
change delivery datemaybedepends
change payment termyesyes

12. Cancellation, Rejection, Void, Reversal, Closure

Istilah ini sering dipakai sembarangan. Dalam ERP, mereka berbeda.

TermKapan DipakaiApakah Ada Efek Finansial/Stock?Mekanisme
Rejectapproval menolak sebelum approvednoterminal or copy-to-new
Canceldibatalkan sebelum irreversible effectusually noclose active workflow/task
Voidmembatalkan dokumen yang belum efektif/legaldependsstrict audit/legal rule
Reversemembalik efek postedyescompensating transaction
Closelifecycle selesaino new effectlock/archive

Prinsip:

Setelah dokumen menghasilkan ledger/stock/legal effect, jangan update history langsung. Gunakan reversal, adjustment, debit note, credit note, atau compensating document.

Contoh invoice:

DRAFT -> APPROVED -> POSTED -> PAID -> CLOSED
POSTED -> CREDITED -> CLOSED
POSTED -> REVERSED -> CLOSED

Jangan:

update invoice set amount = 0 where id = :id;

Gunakan:

create credit note or reversal journal linked to original invoice

13. State Machine Implementation Patterns in Java

13.1 Table-Driven State Machine

Cocok untuk lifecycle yang sederhana dan ingin explicit.

public final class StateMachine<S extends Enum<S>, E extends Enum<E>, D> {
    private final Map<Key<S, E>, Transition<S, E, D>> transitions;

    public TransitionPlan<S> plan(D document, S current, E event, TransitionContext context) {
        Transition<S, E, D> transition = transitions.get(new Key<>(current, event));
        if (transition == null) {
            throw new IllegalTransitionException(current, event);
        }

        GuardDecision guard = transition.guard().evaluate(document, context);
        if (!guard.allowed()) {
            return TransitionPlan.denied(current, event, guard);
        }

        return TransitionPlan.allowed(
                current,
                transition.to(),
                event,
                guard,
                transition.actions()
        );
    }
}

Transition definition:

new Transition<>(
    PurchaseOrderState.DRAFT,
    PurchaseOrderEvent.SUBMIT,
    PurchaseOrderState.SUBMITTED,
    new PurchaseOrderSubmitGuard(),
    List.of(new CreateApprovalWorkflowAction())
)

Kelebihan:

  • mudah dites;
  • mudah direview;
  • tidak perlu framework berat;
  • cocok untuk domain lifecycle.

Kekurangan:

  • workflow kompleks butuh layer tambahan;
  • visualization harus dibuat sendiri.

13.2 State Pattern

Cocok jika setiap state punya behavior signifikan.

public interface PurchaseOrderLifecycleState {
    PurchaseOrderState state();
    TransitionPlan submit(PurchaseOrder po, TransitionContext ctx);
    TransitionPlan approve(PurchaseOrder po, TransitionContext ctx);
    TransitionPlan cancel(PurchaseOrder po, TransitionContext ctx);
}

Risiko: class bisa meledak jika event banyak. Gunakan hanya jika behavior tiap state benar-benar berbeda.

13.3 External State Machine Framework

Framework bisa membantu untuk hierarchical state, guards, actions, dan visualization. Tetapi domain ERP tetap harus punya transition guard sendiri dan transition log sendiri.


14. Hierarchical and Composite States

Beberapa lifecycle lebih mudah dimodelkan dengan state bertingkat.

Contoh Sales Order:

OPEN
  - DRAFT
  - SUBMITTED
  - APPROVED
FULFILLMENT
  - ALLOCATING
  - PICKING
  - SHIPPED_PARTIAL
  - SHIPPED_FULL
BILLING
  - INVOICED_PARTIAL
  - INVOICED_FULL
CLOSED
  - CLOSED_NORMAL
  - CLOSED_CANCELLED
  - CLOSED_REVERSED

Mermaid:

Gunakan hierarchical state jika:

  • banyak sub-state berbagi transition;
  • UI/report butuh status high-level dan detailed;
  • domain punya phase besar.

Jangan gunakan jika hanya membuat model terlihat “canggih”.


15. Parallel State Dimensions

Kadang lifecycle tidak bisa jadi satu state linear.

Sales order bisa punya dimensi:

approval_state: PENDING / APPROVED / REJECTED
fulfillment_state: NOT_RELEASED / RELEASED / PARTIAL / COMPLETE
billing_state: NOT_BILLED / PARTIAL / COMPLETE
payment_state: UNPAID / PARTIAL / PAID

Ini bukan berarti semua kombinasi legal.

Contoh invariant:

billing_state cannot be COMPLETE if fulfillment_state is NOT_RELEASED
payment_state cannot be PAID if billing_state is NOT_BILLED
fulfillment_state cannot be RELEASED if approval_state != APPROVED

Model ini perlu cross-state invariant.

public final class SalesOrderLifecycleInvariant {
    public void validate(SalesOrder order) {
        if (order.fulfillmentState().isReleasedOrBeyond()
                && order.approvalState() != ApprovalState.APPROVED) {
            throw new LifecycleInvariantViolation("Cannot fulfill unapproved order");
        }

        if (order.paymentState() == PaymentState.PAID
                && order.billingState() == BillingState.NOT_BILLED) {
            throw new LifecycleInvariantViolation("Cannot pay unbilled order");
        }
    }
}

16. Concurrency in State Transitions

ERP transition sering bersamaan:

  • approver approve saat requester cancel;
  • scheduler escalate saat user complete task;
  • shipment confirm saat order hold diterapkan;
  • posting worker retry saat user reverse;
  • two warehouse users pick same stock.

16.1 Optimistic Locking

update purchase_order
set state = :nextState,
    version = version + 1
where id = :id
  and state = :expectedState
  and version = :expectedVersion;

Jika affected row = 0, reload dan evaluasi ulang.

16.2 Pessimistic Locking

Dipakai untuk transition high-contention atau high-risk:

select *
from purchase_order
where id = :id
for update;

Tetapi jangan tahan lock saat memanggil external service.

16.3 Idempotent Transition

Command harus punya commandId.

POST /purchase-orders/{id}/approve
Idempotency-Key: actor-task-approval-uuid

Transition log unique:

unique (document_id, command_id)

17. Transition Log as Evidence and Debug Tool

Transition log bukan hanya audit compliance. Ia juga debugging tool.

Minimal event:

{
  "documentId": "...",
  "documentType": "PURCHASE_ORDER",
  "fromState": "IN_REVIEW",
  "event": "APPROVE",
  "toState": "APPROVED",
  "actorId": "...",
  "revision": 4,
  "guardEvidence": {
    "approvalComplete": true,
    "budgetStillAvailable": true,
    "periodOpen": true
  },
  "actionEvidence": {
    "approvalSnapshotId": "...",
    "outboxEventId": "..."
  },
  "transitionTime": "2026-06-30T10:15:30Z",
  "commandId": "..."
}

Pertanyaan audit yang harus bisa dijawab:

  • siapa mengubah state;
  • dari state apa ke state apa;
  • event apa yang memicu;
  • guard apa yang dilalui;
  • revision berapa;
  • apakah ada side effect;
  • apakah transition dilakukan oleh admin correction;
  • apakah transition pernah gagal dan kenapa.

18. Domain Events from State Transitions

State transition sering menghasilkan domain event.

TransitionDomain Event
PO APPROVEDPurchaseOrderApproved
PO POSTEDPurchaseOrderPosted
SO RELEASEDSalesOrderReleased
Invoice POSTEDInvoicePosted
Stock Adjustment POSTEDStockAdjustmentPosted
Work Order COMPLETEDWorkOrderCompleted
Payment FILE_SENTPaymentFileSent

Domain event harus merepresentasikan fakta setelah commit, bukan request sebelum validasi.

public record PurchaseOrderApproved(
        UUID purchaseOrderId,
        String documentNumber,
        int revision,
        UUID legalEntityId,
        Money total,
        Instant approvedAt
) implements DomainEvent {}

Gunakan outbox agar event dan state update commit bersama.


19. Lifecycle and Posting Boundary

Posting adalah state boundary yang sangat penting.

Sebelum posting:

  • dokumen bisa direvisi dengan reapproval;
  • cancellation relatif aman;
  • belum ada efek ledger/stock final.

Sesudah posting:

  • perubahan langsung harus dilarang;
  • correction harus lewat reversal/adjustment;
  • audit requirement meningkat;
  • reporting harus konsisten;
  • downstream reconciliation bergantung pada event ini.

Guard untuk posting:

public final class PostingGuard implements TransitionGuard<FinancialDocument, TransitionContext> {
    @Override
    public GuardDecision evaluate(FinancialDocument doc, TransitionContext ctx) {
        if (doc.approvalState() != ApprovalState.APPROVED) {
            return GuardDecision.deny("POSTING_GUARD", "Document is not approved");
        }
        if (doc.postingState() != PostingState.NOT_POSTED) {
            return GuardDecision.deny("POSTING_GUARD", "Document already posted or reversed");
        }
        if (ctx.periodService().isLocked(doc.accountingDate(), doc.legalEntityId())) {
            return GuardDecision.deny("POSTING_GUARD", "Accounting period is locked");
        }
        if (!doc.isBalancedIfFinancial()) {
            return GuardDecision.deny("POSTING_GUARD", "Journal is not balanced");
        }
        return GuardDecision.allow("POSTING_GUARD");
    }
}

20. Lifecycle Migration

ERP lifecycle berubah saat sistem berkembang. Misalnya dulu PO punya state:

DRAFT -> APPROVED -> POSTED

Lalu ditambahkan:

DRAFT -> SUBMITTED -> IN_REVIEW -> APPROVED -> RELEASED -> POSTED

Migration harus menjawab:

  • instance/dokumen lama state-nya dipetakan ke mana;
  • apakah transition lama tetap valid;
  • apakah report historis berubah;
  • apakah approval lama masih valid;
  • apakah open workflow instance dimigrasikan atau dibiarkan di versi lama;
  • apakah external integration perlu mapping baru.

State migration table:

Old StateNew StateCondition
DRAFTDRAFTalways
APPROVEDAPPROVEDif not posted
APPROVEDRELEASEDif warehouse release exists
POSTEDPOSTEDalways
CANCELLEDCANCELLEDalways

Jangan ubah state historis tanpa migration event.

insert into document_transition_log (
    id, document_id, from_state, to_state, event,
    actor_id, transition_time, revision,
    guard_evidence, action_evidence, command_id
) values (
    gen_random_uuid(), :docId, :oldState, :newState, 'LIFECYCLE_MIGRATION',
    :migrationActor, now(), :revision,
    :evidence, :actionEvidence, :migrationCommandId
);

21. Testing State Machines

State machine harus dites sebagai kontrak.

21.1 Transition Coverage

Pastikan semua legal transition ada test.

@Test
void draftCanBeSubmittedWhenValid() {
    PurchaseOrder po = fixtures.validDraftPo();
    TransitionResult result = lifecycle.transition(po, PurchaseOrderEvent.SUBMIT, context);
    assertThat(result.nextState()).isEqualTo(PurchaseOrderState.SUBMITTED);
}

21.2 Illegal Transition Test

@Test
void closedPoCannotBeEdited() {
    PurchaseOrder po = fixtures.closedPo();
    assertThrows(IllegalTransitionException.class,
        () -> lifecycle.transition(po, PurchaseOrderEvent.EDIT, context));
}

21.3 Guard Failure Test

@Test
void cannotPostWhenPeriodLocked() {
    var po = fixtures.approvedPo();
    var ctx = context.withLockedPeriod(true);

    GuardDeniedException ex = assertThrows(GuardDeniedException.class,
        () -> lifecycle.transition(po, PurchaseOrderEvent.POST, ctx));

    assertThat(ex.reasonCode()).isEqualTo("POSTING_GUARD");
}

21.4 Property-Based Thinking

Invariant yang harus selalu benar:

No document can move out of CLOSED.
No posted document can be edited directly.
No document can be POSTED twice.
No approval decision applies to a different revision.
No transition can skip required approval.

22. State Machine Observability

Metrics:

document_transition_total{document_type,event,from_state,to_state,outcome}
illegal_transition_rejected_total{document_type,event,current_state,reason}
transition_guard_denied_total{guard,reason}
document_state_age_seconds{document_type,state}
posted_document_reversal_total{document_type,reason}
stale_revision_transition_rejected_total{document_type}

Useful dashboards:

  • documents stuck in submitted/in-review;
  • approved but not posted;
  • posted but not integrated;
  • old open documents;
  • high reversal rate;
  • illegal transition attempts;
  • admin corrections by actor;
  • state migration result.

23. UI and API Contract Considerations

UI tidak boleh menebak action dari status saja. Backend harus memberikan allowed actions.

{
  "documentId": "...",
  "state": "IN_REVIEW",
  "revision": 4,
  "allowedActions": [
    {
      "action": "APPROVE",
      "enabled": true,
      "reason": null
    },
    {
      "action": "CANCEL",
      "enabled": false,
      "reason": "Only requester can cancel during review"
    }
  ]
}

Tetapi allowed actions dari UI hanya convenience. Backend tetap harus validasi ulang saat command dikirim.

API command harus membawa expected state/revision/version:

{
  "event": "APPROVE",
  "expectedState": "IN_REVIEW",
  "expectedRevision": 4,
  "expectedVersion": 23,
  "reasonCode": "OK",
  "comment": "Budget verified"
}

24. Anti-Patterns

24.1 One Status to Rule Them All

Satu status untuk approval, posting, integration, dan fulfillment akan menciptakan ambiguity.

24.2 Direct DB Status Patch

Mengubah state langsung tanpa transition log membuat audit dan invariant rusak.

24.3 State Explosion

Terlalu banyak state karena setiap kombinasi status dijadikan state tunggal.

APPROVED_PARTIALLY_SHIPPED_PARTIALLY_INVOICED_PAYMENT_PENDING_EXPORT_FAILED

Gunakan parallel state dimensions jika memang lebih tepat.

24.4 Guard with Side Effects

Guard yang melakukan update, kirim email, atau call external service akan sulit diulang dan sulit dites.

24.5 Framework-Driven Lifecycle Without Domain Semantics

Library state machine bisa membantu, tetapi tidak akan otomatis membuat state punya makna bisnis. Makna harus datang dari domain model.

24.6 No Terminal State Discipline

Dokumen CLOSED yang masih bisa diedit akan menghancurkan audit dan reporting.


25. Worked Example: Purchase Order Lifecycle

25.1 State Diagram

25.2 Invariants

DRAFT can be edited.
SUBMITTED cannot change approval-relevant fields without returning to DRAFT.
APPROVED is valid only for revision N.
RELEASED means supplier-facing commitment exists.
RECEIVED implies at least one goods receipt exists.
CLOSED means no further receiving is allowed.
CANCELLED before receipt has no stock effect.
Cancellation after receipt requires return/reversal/credit flow, not simple cancel.

25.3 Transition Logic

public enum PurchaseOrderEvent {
    SUBMIT,
    START_APPROVAL,
    REQUEST_CHANGE,
    RESUBMIT,
    APPROVE_ALL,
    REJECT,
    RELEASE_TO_SUPPLIER,
    RECEIVE_PARTIAL,
    RECEIVE_COMPLETE,
    CANCEL,
    CLOSE,
    CLOSE_SHORT
}

Transition registration:

registry.register(DRAFT, SUBMIT, SUBMITTED, submitGuard, createApprovalWorkflow);
registry.register(SUBMITTED, START_APPROVAL, IN_REVIEW, workflowGuard, openApprovalTasks);
registry.register(IN_REVIEW, REQUEST_CHANGE, RETURNED, reviewGuard, notifyRequester);
registry.register(RETURNED, RESUBMIT, SUBMITTED, submitGuard, rerouteApproval);
registry.register(IN_REVIEW, APPROVE_ALL, APPROVED, approvalCompleteGuard, snapshotApproval);
registry.register(IN_REVIEW, REJECT, REJECTED, reviewGuard, closeWorkflow);
registry.register(APPROVED, RELEASE_TO_SUPPLIER, RELEASED, releaseGuard, publishSupplierOrder);
registry.register(RELEASED, RECEIVE_PARTIAL, PARTIALLY_RECEIVED, receivingGuard, createStockLedgerEvent);
registry.register(PARTIALLY_RECEIVED, RECEIVE_COMPLETE, FULLY_RECEIVED, receivingGuard, createStockLedgerEvent);
registry.register(FULLY_RECEIVED, CLOSE, CLOSED, closeGuard, closeOpenCommitments);

26. Practice: 20-Hour Lifecycle Drill

Hour 1–2: Pick Three Documents

Pilih:

  • Purchase Order;
  • Sales Order;
  • Journal Entry.

Tuliskan lifecycle state masing-masing.

Hour 3–4: Build State Glossary

Untuk tiap state, tulis:

  • meaning;
  • editable fields;
  • legal exits;
  • forbidden actions;
  • evidence.

Hour 5–7: Transition Matrix

Buat matrix lengkap.

Pastikan ada:

  • submit;
  • approve;
  • reject;
  • cancel;
  • post/release;
  • reverse/close.

Hour 8–10: Implement Table-Driven State Machine

Implement generic state machine sederhana.

Tambahkan:

  • guard;
  • action;
  • transition plan;
  • illegal transition exception.

Hour 11–13: Persistence and Transition Log

Buat schema:

  • document state;
  • transition log;
  • idempotency;
  • optimistic version.

Hour 14–16: Test Failure Scenarios

Simulasikan:

  • double submit;
  • approve stale revision;
  • post locked period;
  • cancel after posting;
  • close with open quantity;
  • reverse twice.

Hour 17–18: Observability

Tambahkan metrics/log:

  • transition success;
  • transition denied;
  • stuck state;
  • stale revision.

Hour 19–20: Architecture Review

Review dengan pertanyaan:

Can this lifecycle survive audit, concurrency, integration retry, and user correction?

27. Design Review Checklist

State Semantics

  • Setiap state punya definisi bisnis eksplisit.
  • Terminal state jelas.
  • Approval, posting, integration, dan fulfillment state tidak dicampur sembarangan.
  • State glossary tersedia.

Transition Rules

  • Semua legal transition ada di matrix.
  • Illegal transition ditolak oleh backend.
  • Guard deterministic dan side-effect free.
  • Action terjadi setelah guard valid.

Audit and Evidence

  • Transition log append-only.
  • Log mencatat actor, event, from/to state, revision, guard evidence.
  • Admin correction tidak menghapus history.
  • Reversal/adjustment dipakai untuk posted effect.

Concurrency

  • Optimistic/pessimistic lock dipakai sesuai risiko.
  • Command idempotent.
  • Expected state/revision/version divalidasi.
  • Race antara scheduler/user/integration sudah dites.

Lifecycle Evolution

  • Workflow/state definition versioned.
  • Migration path untuk state lama jelas.
  • Report historis tidak berubah tanpa explanation.
  • External integration mapping diperhatikan.

28. Key Takeaways

  • State machine ERP adalah kontrak lifecycle, bukan enum status.
  • Bedakan business lifecycle, approval status, posting status, dan integration status.
  • Setiap transition harus punya event, guard, action, next state, dan evidence.
  • Approval harus terikat pada document revision.
  • Setelah posting, correction harus memakai reversal/adjustment, bukan update langsung.
  • Transition log adalah fondasi audit, debugging, dan recovery.
  • State migration harus eksplisit dan tercatat.
  • Framework state machine membantu, tetapi domain semantics tetap harus didesain oleh engineer.

29. Source Notes

Beberapa referensi resmi dan teknis yang relevan untuk part ini:

Lesson Recap

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