Start HereOrdered learning track

Account Model, Balance Types, and Account Lifecycle

Learn Java Core Banking System - Part 006

Account model, balance types, holds, overdraft, restrictions, lifecycle state machine, and account-level invariants for Java-based core banking systems.

17 min read3204 words
PrevNext
Lesson 0635 lesson track0106 Start Here
#java#core-banking#account-model#balance+4 more

Part 006 — Account Model, Balance Types, and Account Lifecycle

Target: mendesain account aggregate yang benar. Fokus: ledger balance, available balance, collected balance, blocked balance, overdraft, dormant account, closed account, dan state transition.

Pada Part 005 kita membangun fondasi ledger: money, journal, posting line, debit/credit, dan balanced journal. Sekarang kita naik satu layer ke domain yang dilihat nasabah dan operator: account.

Di core banking, account bukan sekadar row dengan account_number dan balance. Account adalah kontrak operasional yang menghubungkan:

  • party/customer,
  • product,
  • agreement,
  • ledger,
  • balance view,
  • restrictions,
  • lifecycle state,
  • operational controls,
  • audit trail.

Kesalahan paling mahal dalam account model biasanya bukan syntax Java. Kesalahannya adalah model mental: menganggap “saldo” hanya satu angka dan “rekening” hanya punya status aktif/nonaktif.


1. Kaufman Deconstruction untuk Account Skill

Sub-skill yang harus dikuasai:

Sub-skillPertanyaan self-correction
Account identityApakah account number, account id, dan ledger account id saya pisahkan?
Balance taxonomyApakah saya bisa menjelaskan ledger, available, collected, blocked, overdraft, dan pending balance?
Lifecycle stateApakah semua transisi account eksplisit dan tervalidasi?
RestrictionsApakah freeze, debit block, credit block, lien, dan dormancy tidak dicampur jadi satu boolean?
Posting eligibilityApakah setiap transaction type mengecek state dan restriction yang relevan?
Balance derivationApakah balance berasal dari ledger/projection yang bisa direkonsiliasi?
AuditabilityApakah perubahan limit/status/restriction punya actor, reason, approval, dan timestamp?

Tujuan setelah part ini:

Anda bisa mendesain account aggregate yang tidak hanya menyimpan saldo, tetapi menjaga lifecycle, balance semantics, restrictions, posting eligibility, dan audit evidence.


2. Account Bukan Ledger, tetapi Terhubung ke Ledger

Account adalah domain object produk/kontrak. Ledger adalah pencatatan accounting.

Contoh:

Customer Account:
- account_id: ACC-001
- account_number: 1234567890
- product_code: SAVINGS_REGULAR
- owner_party_id: P-001
- status: ACTIVE

Ledger Account:
- ledger_account_id: LEDGER-CUST-ACC-001
- account_type: LIABILITY
- gl_account_code: CUSTOMER_DEPOSITS_LIABILITY

Kenapa dipisahkan?

Karena account bisa berubah status, produk, owner, restriction, dan limit; sedangkan ledger postings adalah catatan nilai yang immutable. Memisahkan keduanya membuat sistem lebih mudah diaudit dan lebih aman saat produk berevolusi.


3. Account Identity: Jangan Campur Semua ID

Minimal identity yang perlu dipisahkan:

IdentityDipakai untukBoleh berubah?
accountIdInternal immutable identifierTidak
accountNumberNomor rekening eksternal/customer-facingIdealnya tidak, tapi bisa ada renumbering/masking scenario
ledgerAccountIdNode ledger/subledgerTidak untuk historical posting
agreementIdKontrak hukum/produkBisa diganti melalui lifecycle tertentu
partyIdPemilik/relasi customerBisa berubah melalui ownership maintenance yang diaudit
productCodeProduct templateBisa migration/upgrade dengan effective date

Anti-pattern:

String accountNumber; // used as primary key everywhere

Kenapa buruk?

  • account number bisa punya format channel-specific,
  • bisa perlu masking/tokenization,
  • bisa conflict saat migration,
  • sulit melakukan account renumbering,
  • historical posting sebaiknya mereferensikan immutable id.

Model lebih aman:

public record AccountId(String value) {}
public record AccountNumber(String value) {}
public record LedgerAccountId(String value) {}
public record PartyId(String value) {}
public record ProductCode(String value) {}

4. Core Account Aggregate

Account aggregate jangan terlalu gemuk. Ia harus menyimpan state yang menentukan apakah transaksi boleh dilakukan, bukan menghitung semua hal sendiri.

public final class Account {
    private final AccountId accountId;
    private final AccountNumber accountNumber;
    private final PartyId ownerPartyId;
    private final ProductCode productCode;
    private final LedgerAccountId ledgerAccountId;
    private AccountStatus status;
    private AccountRestrictions restrictions;
    private AccountLimits limits;
    private AccountDates dates;
    private long version;

    public Account(
        AccountId accountId,
        AccountNumber accountNumber,
        PartyId ownerPartyId,
        ProductCode productCode,
        LedgerAccountId ledgerAccountId,
        AccountStatus status,
        AccountRestrictions restrictions,
        AccountLimits limits,
        AccountDates dates,
        long version
    ) {
        this.accountId = accountId;
        this.accountNumber = accountNumber;
        this.ownerPartyId = ownerPartyId;
        this.productCode = productCode;
        this.ledgerAccountId = ledgerAccountId;
        this.status = status;
        this.restrictions = restrictions;
        this.limits = limits;
        this.dates = dates;
        this.version = version;
    }

    public void assertCanDebit(Money amount, BalanceView balanceView) {
        status.assertDebitAllowed();
        restrictions.assertDebitAllowed();
        limits.assertDebitWithinLimit(amount, balanceView);
    }

    public void assertCanCredit(Money amount) {
        status.assertCreditAllowed();
        restrictions.assertCreditAllowed();
    }

    public void freeze(RestrictionReason reason, Actor actor) {
        restrictions = restrictions.withFullFreeze(reason, actor);
        version++;
    }

    public void close(Actor actor, ClosureReason reason, BalanceView balanceView) {
        if (!balanceView.isZero()) {
            throw new IllegalStateException("account must have zero balance before closure");
        }
        status = AccountStatus.CLOSED;
        version++;
    }
}

Catatan:

  • Account memvalidasi eligibility.
  • Posting tetap dilakukan oleh posting engine.
  • BalanceView adalah projection yang digunakan untuk keputusan, bukan sumber mutasi sembarang.
  • Perubahan restriction/status harus diaudit.

5. Balance Taxonomy: “Saldo” Bukan Satu Angka

Dalam core banking, “saldo” bisa berarti banyak hal.

Balance typeMaknaDipakai untuk
Ledger/book/current balanceSaldo dari posted ledger entrieslaporan, statement, audit
Available balanceDana yang bisa digunakan sekarangtransfer, withdrawal, debit authorization
Collected balanceDana yang sudah cleared/collectedinterest, availability, risk
Blocked/held amountDana yang ditahancard hold, lien, court order, fraud review
Pending debitDebit yang sudah diterima tapi belum final postedpayment processing, authorization
Pending creditCredit yang diharapkan tetapi belum finalincoming transfer pending
Overdraft limitBatas tambahan yang diizinkancurrent account, corporate account
Minimum balanceSaldo minimum produkfee/eligibility
Accrued interestInterest yang sudah dihitung tetapi belum dikapitalisasiaccrual/reporting
Uncleared fundsDana belum settle/clearcheck deposit, external payment

Formula sederhana untuk deposit account:

available_balance = ledger_balance
                  - active_holds
                  - pending_debits
                  - minimum_required_balance
                  + allowed_overdraft

Namun formula final tergantung produk, currency, regulator, channel, dan transaction type.


6. Balance View Model

public record BalanceView(
    AccountId accountId,
    String currency,
    Money ledgerBalance,
    Money availableBalance,
    Money collectedBalance,
    Money blockedAmount,
    Money pendingDebitAmount,
    Money pendingCreditAmount,
    Money overdraftLimit,
    LocalDate businessDate,
    Instant calculatedAt,
    long ledgerSequence
) {
    public boolean isZero() {
        return ledgerBalance.amount().compareTo(BigDecimal.ZERO) == 0;
    }

    public boolean hasAvailableAtLeast(Money amount) {
        requireSameCurrency(amount);
        return availableBalance.amount().compareTo(amount.amount()) >= 0;
    }

    private void requireSameCurrency(Money amount) {
        if (!currency.equals(amount.currency())) {
            throw new IllegalArgumentException("currency mismatch");
        }
    }
}

Poin penting:

  • ledgerSequence membantu memastikan balance view berasal dari posting sampai sequence tertentu.
  • calculatedAt membantu observability dan debugging stale projection.
  • businessDate membantu EOD/reporting.
  • Jangan campur availableBalance dengan ledgerBalance.

7. Ledger Balance vs Available Balance

Misal saldo ledger IDR 1,000,000. Ada hold card authorization IDR 300,000. Ada minimum balance IDR 50,000.

ledger_balance    = 1,000,000
active_holds      =   300,000
minimum_balance   =    50,000
available_balance =   650,000

Jika customer melihat saldo di mobile banking, UI bisa menampilkan:

Current balance:   IDR 1,000,000
Available balance: IDR   650,000

Jika sistem hanya punya satu field balance, salah satu akan terjadi:

  • customer bisa menarik dana yang seharusnya blocked,
  • customer mengira uang hilang,
  • reconciliation card/payment gagal,
  • complaint sulit dijelaskan.

8. Hold, Block, Lien, dan Freeze: Jangan Jadi Satu Boolean

Restriction punya banyak jenis.

RestrictionScopeDebit?Credit?Contoh
Debit blockAccountTidakYarekening sedang review fraud
Credit blockAccountYaTidakaccount tidak boleh menerima dana
Full freezeAccountTidakTidakcourt order, compliance freeze
Amount holdAmountTergantung sisa availableYacard authorization hold
LienAmount atau fullMengurangi availableYacollateral, legal hold
Dormant restrictionAccountBiasanya tidakTergantung policydormant account
Product restrictionProduct/ruleTergantungTergantungaccount type tidak support transfer

Model:

public record AccountRestrictions(
    boolean debitBlocked,
    boolean creditBlocked,
    boolean fullyFrozen,
    List<AmountHold> amountHolds,
    List<Lien> liens,
    List<RestrictionRecord> records
) {
    public void assertDebitAllowed() {
        if (fullyFrozen) {
            throw new IllegalStateException("account is fully frozen");
        }
        if (debitBlocked) {
            throw new IllegalStateException("account is debit blocked");
        }
    }

    public void assertCreditAllowed() {
        if (fullyFrozen) {
            throw new IllegalStateException("account is fully frozen");
        }
        if (creditBlocked) {
            throw new IllegalStateException("account is credit blocked");
        }
    }
}

public record AmountHold(
    String holdId,
    Money amount,
    HoldType type,
    Instant createdAt,
    Optional<Instant> expiresAt,
    String reason,
    String sourceReference
) {}

Rule:

  • Restriction harus punya reason.
  • Restriction harus punya actor/source.
  • Restriction harus punya effective time.
  • Pelepasan restriction juga harus diaudit.
  • Hold expiry harus deterministik dan dapat diproses ulang.

9. Pending vs Posted

Pending transaction tidak sama dengan posted transaction.

Contoh card authorization:

  1. Customer transaksi di merchant IDR 200,000.
  2. Core/card boundary membuat hold IDR 200,000.
  3. Available balance turun.
  4. Ledger balance belum tentu berubah jika belum captured/settled.
  5. Saat settlement datang, hold dilepas dan posting final dibuat.

Jika sistem langsung mem-post ledger pada authorization sementara settlement belum final, reversal/partial capture/tips/foreign exchange bisa menjadi rumit. Namun jika sistem tidak menahan available balance, customer bisa overspend.

Karena itu hold/reservation adalah konsep terpisah dari posting final.


10. Account Lifecycle State Machine

Account lifecycle harus eksplisit.

Minimal states:

StateMeaningDebitCreditNotes
PendingOpeningAccount dibuat tetapi belum aktifTidakTidakmenunggu KYC/approval/funding
ActiveNormal operationsYaYasesuai product rule
RestrictedAda restriction aktifTergantungTergantunglihat restriction detail
DormantInactive berdasarkan policyBiasanya tidakpolicy-dependentreactivation perlu control
PendingClosureProses penutupanTerbatasTerbataspastikan zero/settlement selesai
ClosedAccount ditutupTidakTidakhanya boleh historical query
RejectedOpening gagalTidakTidaktidak boleh posting

Jangan cukup pakai:

boolean active;

Karena “not active” bisa berarti pending, dormant, restricted, closed, rejected, atau blocked. Masing-masing punya aturan yang berbeda.


11. Transaction Eligibility Matrix

Setiap transaction type harus punya rule terhadap account state dan restriction.

TransactionActiveRestrictedDormantPendingClosureClosed
Cash depositAllowedDepends credit blockUsually controlledUsually noNo
Cash withdrawalAllowedDepends debit blockNo until reactivatedNoNo
Internal transfer debitAllowedDepends debit blockNoNoNo
Internal transfer creditAllowedDepends credit blockDepends policyNoNo
Interest accrualAllowedUsually allowedUsually allowedDependsNo new accrual after closure
Fee assessmentAllowedDependsDepends product/regulationDependsNo
ReversalAllowedSpecial pathSpecial pathSpecial pathSpecial path with control
Closure payoutAllowedControlledControlledAllowedNo

Critical nuance:

Reversal dan correction sering membutuhkan special path. Kalau account sudah restricted atau closed, sistem tetap mungkin harus mengizinkan correction yang tidak sama dengan transaksi normal.

Contoh:

  • Salah posting fee pada account yang sekarang closed.
  • Reversal harus bisa membuat accounting correction melalui controlled operational process.
  • Ini tidak berarti account boleh dipakai normal lagi.

12. Posting Eligibility Flow

Reject reason harus spesifik:

ACCOUNT_CLOSED
ACCOUNT_DORMANT_REACTIVATION_REQUIRED
DEBIT_BLOCKED
CREDIT_BLOCKED
INSUFFICIENT_AVAILABLE_BALANCE
AMOUNT_HELD
PRODUCT_DOES_NOT_ALLOW_TRANSACTION_TYPE
BUSINESS_DATE_CLOSED

Jangan return generic:

TRANSACTION_FAILED

Dalam bank, reject reason adalah bagian dari evidence dan customer-service explainability.


13. Account Limits dan Overdraft

Overdraft bukan negative balance bug. Overdraft adalah product feature yang harus eksplisit.

public record AccountLimits(
    Money dailyDebitLimit,
    Money singleTransactionLimit,
    Money overdraftLimit,
    Money minimumRequiredBalance
) {
    public void assertDebitWithinLimit(Money amount, BalanceView balanceView) {
        if (!balanceView.hasAvailableAtLeast(amount)) {
            throw new IllegalStateException("insufficient available balance");
        }
    }
}

Namun formula available balance untuk overdraft account mungkin:

available = ledger_balance - holds - pending_debits - minimum_balance + overdraft_limit

Jika ledger balance IDR 100,000 dan overdraft limit IDR 500,000, available bisa IDR 600,000. Tapi jika product tidak mengizinkan overdraft, available tidak boleh menjadi negatif.

Rule:

  • Overdraft limit harus effective-dated.
  • Perubahan limit harus maker-checker untuk konteks tertentu.
  • Overdraft usage harus bisa dipantau.
  • Interest/fee atas overdraft adalah product engine concern.
  • Limit tidak boleh hardcoded di channel.

14. Account Closure

Closure bukan status = CLOSED.

Closure harus memastikan:

  • ledger balance zero atau diselesaikan melalui payout/write-off,
  • active holds dilepas atau diselesaikan,
  • pending transactions selesai/expired/cancelled,
  • accrued interest/fee dihitung,
  • tax/withholding selesai,
  • card/payment instrument linked dinonaktifkan,
  • standing instruction/direct debit dihentikan,
  • statement/final advice dibuat,
  • closure reason dan actor diaudit.

Anti-pattern:

update account set status = 'CLOSED' where account_number = ?;

Dampak:

  • ada pending debit masuk setelah closure,
  • interest accrual masih jalan,
  • statement tidak final,
  • reconciliation muncul break,
  • complaint tidak bisa dijawab.

15. Dormancy dan Reactivation

Dormant account biasanya muncul karena tidak ada aktivitas customer dalam periode tertentu. Dormancy bukan sekadar status teknis; ia punya konsekuensi operasional dan regulatory/local-policy.

Model minimal:

public record DormancyPolicy(
    ProductCode productCode,
    Period inactivityPeriod,
    boolean allowCreditWhileDormant,
    boolean allowSystemAccrualWhileDormant,
    boolean requireReactivationApproval
) {}

Dormancy detection:

last_customer_initiated_activity_date + inactivity_period <= business_date

Jangan gunakan semua posting sebagai activity. Interest accrual, fee, dan system adjustment bisa membuat account terlihat aktif padahal tidak ada aktivitas customer.

Perlu bedakan:

Activity typeMenghentikan dormancy?
Customer cash depositYa
Customer withdrawalYa
Customer transferYa
Interest accrualBiasanya tidak
Fee postingBiasanya tidak
System correctionBiasanya tidak

Reactivation harus punya:

  • actor,
  • reason,
  • evidence,
  • approval jika diwajibkan,
  • timestamp,
  • channel/source.

16. Account Statement dan Balance Explanation

Account statement bukan laporan kosmetik. Ia adalah customer-facing explanation dari ledger.

Statement line biasanya berasal dari posting/journal, tetapi disajikan dari perspektif customer.

Untuk deposit account:

Accounting sideCustomer statement effect
Credit liabilityBalance increases
Debit liabilityBalance decreases

Untuk loan account:

Accounting sideCustomer loan balance effect
Debit loan assetOutstanding principal increases
Credit loan assetOutstanding principal decreases

Karena itu statement generation harus tahu account type dan product perspective.

public enum CustomerBalanceEffect {
    INCREASE,
    DECREASE
}

public CustomerBalanceEffect toCustomerEffect(AccountType accountType, PostingSide side) {
    return switch (accountType) {
        case LIABILITY -> side == PostingSide.CREDIT
            ? CustomerBalanceEffect.INCREASE
            : CustomerBalanceEffect.DECREASE;
        case ASSET -> side == PostingSide.DEBIT
            ? CustomerBalanceEffect.INCREASE
            : CustomerBalanceEffect.DECREASE;
        default -> throw new IllegalArgumentException("not a customer-facing account type");
    };
}

17. Balance Projection Consistency

Balance projection harus bisa direkonsiliasi dengan posting lines.

Projection model:

account_balance_projection
- account_id
- currency
- ledger_balance
- available_balance
- collected_balance
- blocked_amount
- pending_debit_amount
- pending_credit_amount
- business_date
- last_journal_sequence
- updated_at

Invariant:

projection.ledger_balance == derived balance from posting lines up to last_journal_sequence

Jika tidak cocok, sistem harus menandai anomaly, bukan diam-diam overwrite.

Rebuild strategy:

  1. Ambil opening snapshot yang sudah certified.
  2. Replay posting lines setelah snapshot sequence.
  3. Recompute balance projection.
  4. Compare dengan current projection.
  5. Jika mismatch, create recon break.

18. Account-Level Serialization

Walaupun seri concurrency sudah selesai, account model butuh satu prinsip domain:

Semua mutasi yang memengaruhi available balance account yang sama harus memiliki ordering yang deterministik.

Contoh race:

Ledger balance = IDR 100,000
Transfer A debit = IDR 80,000
Transfer B debit = IDR 80,000

Jika dua command membaca available balance yang sama secara paralel, keduanya bisa lolos. Hasilnya overdraft tidak sengaja.

Solusi domain-level:

  • account-level lock,
  • optimistic version check,
  • serialized command queue per account,
  • reservation/hold before posting,
  • database constraint/projection update atomic.

Yang penting: jangan hanya mengandalkan “service stateless” jika keputusan balance membutuhkan current state.


19. Account Event Model

Account lifecycle menghasilkan domain events yang berbeda dari accounting events.

EventJenisApakah posting ledger?
AccountOpenedAccount lifecycleTidak selalu
AccountActivatedAccount lifecycleTidak
AccountRestrictedControl eventTidak
HoldPlacedReservation/controlTidak selalu
HoldReleasedReservation/controlTidak selalu
AccountDormantMarkedLifecycle/controlTidak
AccountReactivatedLifecycle/controlTidak
AccountClosureRequestedLifecycleTidak
AccountClosedLifecycleTidak, kecuali closure payout/fee sudah diposting
JournalPostedAccounting event resultYa

Jangan campur semua menjadi TransactionEvent.


20. Restrictions as Evidence

Setiap restriction harus bisa menjawab:

Who placed it?
When?
From which channel/system?
Under what authority?
For what reason?
What scope?
What amount?
When does it expire?
Who removed it?
Why was it removed?

Model:

public record RestrictionRecord(
    String restrictionId,
    RestrictionType type,
    RestrictionScope scope,
    Optional<Money> amount,
    String reasonCode,
    String narrative,
    Actor placedBy,
    Instant placedAt,
    Optional<Actor> removedBy,
    Optional<Instant> removedAt,
    Optional<String> removalReason
) {}

public enum RestrictionType {
    DEBIT_BLOCK,
    CREDIT_BLOCK,
    FULL_FREEZE,
    AMOUNT_HOLD,
    LIEN,
    DORMANCY_BLOCK
}

If it affects customer money, it needs evidence.


21. Account Opening

Account opening flow:

Important points:

  • Account opening may not post ledger yet.
  • Initial deposit is separate accounting event.
  • Product eligibility should be evaluated against product version.
  • Agreement and account should reference effective terms.
  • Ledger account mapping must exist before monetary posting.

Anti-pattern:

Opening account automatically creates fake zero transaction.

Zero transaction sering menjadi noise. Kalau dibutuhkan untuk audit/statement, gunakan explicit lifecycle event, bukan fake financial posting.


22. Account Number Generation

Account number generation harus mempertimbangkan:

  • uniqueness,
  • check digit,
  • branch/product prefix jika diperlukan,
  • privacy/masking,
  • migration compatibility,
  • not exposing internal sequence predictably if risk-sensitive,
  • formatting across channels.

Jangan menjadikan account number sebagai source of truth untuk routing internal jika ada immutable account id.

public interface AccountNumberGenerator {
    AccountNumber next(ProductCode productCode, BranchCode branchCode);
}

Check digit membantu mendeteksi typo, tapi bukan security mechanism.


23. Joint Account dan Relationship Complexity

Account tidak selalu punya satu owner sederhana.

Relasi umum:

RelationshipMeaning
Primary ownerPemilik utama
Joint ownerPemilik bersama
Authorized signerBoleh melakukan transaksi tertentu
GuardianBertindak untuk minor/dependent
Corporate representativeBertindak atas nama perusahaan
Beneficial ownerRelevan untuk compliance/KYC

Model:

public record AccountPartyRelationship(
    AccountId accountId,
    PartyId partyId,
    AccountPartyRole role,
    LocalDate effectiveFrom,
    Optional<LocalDate> effectiveTo,
    AuthorizationProfile authorizationProfile
) {}

public enum AccountPartyRole {
    PRIMARY_OWNER,
    JOINT_OWNER,
    AUTHORIZED_SIGNER,
    GUARDIAN,
    CORPORATE_REPRESENTATIVE,
    BENEFICIAL_OWNER
}

Posting eligibility bisa tergantung actor authority, bukan hanya account status.

Contoh:

  • Authorized signer boleh transfer sampai limit tertentu.
  • Joint account butuh single-sign atau dual-sign sesuai mandate.
  • Corporate account bisa butuh approval matrix.

24. Account Product Binding

Account dibuat berdasarkan product version. Jangan hanya simpan product code tanpa version/effective terms.

public record AccountProductBinding(
    AccountId accountId,
    ProductCode productCode,
    String productVersion,
    LocalDate effectiveFrom,
    Optional<LocalDate> effectiveTo
) {}

Kenapa penting?

  • Fee berubah mulai bulan depan.
  • Minimum balance berubah.
  • Dormancy policy berubah.
  • Interest calculation berubah.
  • Account lama mungkin grandfathered.

Tanpa versioning, Anda tidak bisa menjelaskan kenapa dua account dengan product code sama diperlakukan berbeda.


25. Account Statement Period and Opening/Closing Balance

Untuk statement period:

opening_balance + credits - debits = closing_balance

Namun credits/debits dari statement perspective harus disesuaikan dengan account type.

Statement period model:

public record StatementPeriod(
    AccountId accountId,
    LocalDate fromInclusive,
    LocalDate toInclusive,
    Money openingBalance,
    Money totalCredits,
    Money totalDebits,
    Money closingBalance,
    List<StatementLine> lines
) {}

Invariant:

opening + customer_credits - customer_debits == closing

Ini berbeda dari journal invariant:

total accounting debit == total accounting credit

Keduanya benar di perspektif yang berbeda.


26. Handling Closed Account Corrections

Real system harus menangani koreksi setelah account closed.

Contoh:

  • Fee salah charge sebelum account closed.
  • Interest kurang bayar.
  • External return datang terlambat.
  • Settlement adjustment masuk setelah closure.

Pilihan desain:

  1. Reopen account under controlled process.
  2. Post correction to suspense/settlement account and create case.
  3. Create payable/receivable outside original customer account.
  4. Use special closed-account correction flow.

Jangan diam-diam mengubah status closed menjadi active tanpa evidence.

Decision table:

ScenarioRecommended handling
Customer owed small refundCreate payable/refund case
Bank owed amount after closureCreate receivable/recovery case
Regulatory correction requires account statement updateControlled reopen or historical correction process
Late external settlement returnSuspense + investigation + controlled posting

27. Account Data Model Minimal

account
- account_id
- account_number
- owner_party_id
- product_code
- product_version
- ledger_account_id
- status
- branch_code
- currency
- opened_date
- activated_date
- closed_date nullable
- closure_reason nullable
- version
- created_at
- updated_at

account_restriction
- restriction_id
- account_id
- type
- scope
- amount nullable
- currency nullable
- reason_code
- narrative
- placed_by
- placed_at
- removed_by nullable
- removed_at nullable
- removal_reason nullable

account_balance_projection
- account_id
- currency
- ledger_balance
- available_balance
- collected_balance
- blocked_amount
- pending_debit_amount
- pending_credit_amount
- overdraft_limit
- business_date
- last_journal_sequence
- updated_at

account_party_relationship
- account_id
- party_id
- role
- authorization_profile_id
- effective_from
- effective_to nullable

Jangan anggap ini final schema. Ini domain checklist.


28. Account Invariants Checklist

Account identity:

  • accountId immutable.
  • accountNumber unique within required namespace.
  • ledgerAccountId exists before financial posting.
  • product binding references product version.

Lifecycle:

  • closed account cannot receive normal debit/credit.
  • pending opening cannot transact.
  • dormant account needs reactivation for customer-initiated debit.
  • closure requires zero or controlled settlement.
  • status transition requires reason and actor.

Balance:

  • available balance never confused with ledger balance.
  • holds reduce available balance.
  • overdraft is explicit product feature.
  • balance projection can be rebuilt from ledger.
  • stale projection detectable via sequence/timestamp.

Restrictions:

  • debit block and credit block are separate.
  • amount holds have expiry/source/reference.
  • restriction placement/removal audited.
  • special correction path exists for restricted/closed account.

Operations:

  • reject reasons are specific.
  • statement can explain balance movement.
  • account event and accounting event are separate.
  • migration preserves account identity and ledger mapping.

29. Anti-Patterns Account Model

29.1 Single balance field

Problem:

balance = 1000000

Tidak jelas apakah ini ledger, available, collected, or projected.

29.2 isActive boolean

Problem:

boolean isActive;

Tidak bisa membedakan dormant, frozen, pending, restricted, closed.

29.3 Account number as primary key everywhere

Problem:

  • migration sulit,
  • masking sulit,
  • renumbering sulit,
  • external reference bocor ke internal architecture.

29.4 Editing balance manually

Problem:

  • ledger tidak cocok,
  • reconciliation break,
  • audit lemah.

Correct approach:

Correction must be posted as journal entry.

29.5 Restriction as free-text note

Problem:

note = "blocked by ops"

Tidak bisa dieksekusi secara deterministik. Restriction harus structured.


30. Baeldung-Style Implementation Example: Debit Validation

public final class DebitAuthorizationService {
    private final AccountRepository accountRepository;
    private final BalanceQuery balanceQuery;
    private final ProductRuleService productRuleService;

    public DebitAuthorizationService(
        AccountRepository accountRepository,
        BalanceQuery balanceQuery,
        ProductRuleService productRuleService
    ) {
        this.accountRepository = accountRepository;
        this.balanceQuery = balanceQuery;
        this.productRuleService = productRuleService;
    }

    public DebitAuthorization authorize(DebitCommand command) {
        Account account = accountRepository.get(command.accountId());
        BalanceView balance = balanceQuery.currentBalance(command.accountId(), command.currency());

        try {
            account.assertCanDebit(command.amount(), balance);
            productRuleService.assertTransactionAllowed(
                account.productCode(),
                TransactionType.DEBIT_TRANSFER,
                command.amount()
            );
            return DebitAuthorization.approved(command.commandId());
        } catch (IllegalStateException ex) {
            return DebitAuthorization.rejected(
                command.commandId(),
                RejectReason.from(ex)
            );
        }
    }
}

This service does not post ledger. It only authorizes debit eligibility. Posting happens after accounting event construction.

Separation:

DebitAuthorizationService -> decides whether debit can proceed
AccountingEventBuilder    -> builds debit/credit lines
PostingEngine             -> atomically posts journal

31. Testing Strategy

Test account model dengan scenario, bukan hanya getter/setter.

31.1 State transition tests

PendingOpening cannot debit.
Active can debit if no restriction and sufficient available.
Dormant cannot customer-debit before reactivation.
Closed cannot normal credit.
PendingClosure cannot new debit except closure payout.

31.2 Balance tests

ledger=1,000,000 hold=300,000 min=50,000 -> available=650,000
ledger=100,000 overdraft=500,000 -> available=600,000 if overdraft enabled
same scenario -> available=100,000 if overdraft disabled

31.3 Restriction tests

Debit block rejects debit but allows credit.
Credit block rejects credit but allows debit.
Full freeze rejects both.
Amount hold reduces available.
Expired hold no longer reduces available after controlled expiry process.

31.4 Correction tests

Closed account rejects normal transfer.
Closed account allows controlled reversal path with approval.
Restricted account allows system correction when policy permits.

32. Deliberate Practice

Extend mini-ledger dari Part 005:

  1. Buat Account dengan status PENDING_OPENING, ACTIVE, DORMANT, RESTRICTED, PENDING_CLOSURE, CLOSED.
  2. Implement BalanceView dengan ledger, available, blocked, pending debit, overdraft.
  3. Implement debit authorization.
  4. Implement credit authorization.
  5. Implement amount hold placement and release.
  6. Implement account closure validation.
  7. Implement dormancy detection based on last customer activity.
  8. Implement reject reason yang spesifik.
  9. Buat test matrix untuk transaction eligibility.
  10. Buat function explainAvailableBalance(accountId) yang menjelaskan komponen available balance.

Expected output:

Available Balance Explanation
Account: 1234567890
Currency: IDR
Ledger Balance:      1,000,000
Active Holds:         -300,000
Pending Debits:              0
Minimum Balance:       -50,000
Overdraft Limit:             0
Available Balance:     650,000

Jika Anda bisa membuat explanation seperti itu dari model sendiri, Anda mulai berpikir seperti engineer core banking, bukan hanya CRUD engineer.


33. Summary

Account model yang benar harus memisahkan:

Account identity
Account lifecycle
Account restrictions
Account balance views
Ledger postings
Product rules
Party relationships
Operational evidence

Core rule:

Account tidak boleh menjadi satu row besar dengan satu status dan satu balance. Account adalah lifecycle-bearing domain object yang mengontrol eligibility dan menjembatani customer/product dengan ledger.

Mental model akhir:

Jika Part 005 menjawab “bagaimana nilai dicatat”, Part 006 menjawab “rekening mana yang boleh terkena transaksi, saldo apa yang boleh digunakan, dan state apa yang membuat transaksi sah atau tidak sah”.


References

  • ISO — ISO 4217 Currency Codes: currency code and minor unit metadata.
  • Basel Committee on Banking Supervision — BCBS 239: accuracy, completeness, timeliness, and data lineage principles relevant to balance/reporting evidence.
  • Oracle FLEXCUBE Universal Banking documentation: examples of account class/account maintenance concepts in commercial core banking systems.
  • BIAN Service Landscape: account, product, customer, and operational capability decomposition.
Lesson Recap

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