Build CoreOrdered learning track

Asset, Project, and Service Management

Learn Java Large Scale ERP - Part 015

Deep dive into asset, project, and service management domains in large-scale Java ERP, including asset lifecycle, depreciation boundary, project accounting, service order lifecycle, maintenance, warranty, cost capture, auditability, integration, and failure modelling.

21 min read4192 words
PrevNext
Lesson 1534 lesson track0718 Build Core
#java#erp#asset-management#project-accounting+5 more

Part 015 — Asset, Project, and Service Management

1. Target Skill Part Ini

Asset, project, dan service management sering terlihat seperti modul tambahan di ERP. Dalam sistem besar, tiga domain ini adalah jembatan antara physical reality, financial accountability, dan customer/internal service obligation.

Skill inti part ini: mampu mendesain domain asset, project, dan service dalam Java ERP sehingga acquisition, capitalization, depreciation handoff, maintenance, project cost capture, project billing, service order, warranty, SLA, spare-part usage, labor capture, dan audit evidence tetap konsisten walaupun ada partial completion, retroactive adjustment, asset transfer, component replacement, project reclassification, service cancellation, warranty dispute, dan integration failure.

Asset bukan sekadar tabel fixed_asset.

Project bukan sekadar project_code di invoice.

Service bukan sekadar tiket.

Dalam ERP besar, ketiganya saling memengaruhi:

  • pembelian mesin menjadi asset setelah capitalization;
  • asset membutuhkan preventive dan corrective maintenance;
  • maintenance memakai spare part dari inventory;
  • biaya maintenance masuk ke expense, asset improvement, project, atau warranty claim;
  • project mengumpulkan cost dari procurement, timesheet, inventory issue, expense, dan service execution;
  • service order bisa menghasilkan invoice, warranty claim, replacement part, atau internal cost;
  • fixed asset depreciation harus masuk GL;
  • project WIP harus direkonsiliasi;
  • service SLA breach harus terlihat dan bisa diaudit.

Jika desainnya buruk:

  • asset sudah dipakai tetapi belum dikapitalisasi;
  • depreciation berjalan atas nilai yang salah;
  • project cost tersebar di banyak modul tanpa traceability;
  • maintenance cost tidak bisa dibedakan antara repair dan capital improvement;
  • warranty part diganti tetapi inventory tidak berkurang;
  • service order closed walaupun labor/spare part belum lengkap;
  • project profitability tidak bisa dipercaya;
  • asset disposal tidak menghapus asset dari operational availability;
  • audit tidak bisa membuktikan siapa mengubah useful life atau residual value.

Part ini membangun asset/project/service sebagai lifecycle domains yang berinteraksi dengan procurement, inventory, accounting, HR/time capture, billing, workflow, dan reporting.


2. Kaufman Deconstruction: Memecah Skill Asset, Project, dan Service ERP

Menurut pendekatan Kaufman, skill kompleks harus dipecah menjadi sub-skill yang dapat dilatih secara terpisah. Untuk part ini, pecahannya adalah:

Sub-skillPertanyaan yang Harus Bisa DijawabOutput Engineering
Asset identityApa yang membuat asset unik secara legal/operasional?asset master, tag, serial, location, custodian
Asset lifecycleBagaimana asset bergerak dari request sampai disposal?state machine, transition guard, audit trail
CapitalizationKapan cost menjadi asset, bukan expense?capitalization event, source cost linkage
Depreciation boundarySiapa menghitung, siapa posting, siapa approve?depreciation schedule, posting request, adjustment
Asset hierarchyBagaimana component/sub-asset dilacak?parent-child asset tree, replacement history
MaintenanceBagaimana preventive/corrective maintenance dijalankan?work order, schedule, labor, spare part, downtime
Project cost captureDari mana project cost berasal?cost transaction, cost bucket, source traceability
Project accountingKapan cost menjadi expense, WIP, revenue, capitalization?project ledger, billing milestone, recognition boundary
Service orderBagaimana permintaan service dieksekusi?ticket/order lifecycle, SLA, assignment, closure
WarrantyKapan biaya ditanggung warranty?warranty policy, entitlement, claim, coverage decision
Cross-module integrationApa efek ke inventory, GL, AP, AR, procurement?event contracts, posting pipeline, reconciliation
Failure modellingBagaimana recover dari partial/incorrect execution?reversal, correction, exception queue, compensating action

Tujuan belajar bukan menghafal field. Tujuannya mampu menjawab:

“Ketika asset, project, atau service berubah status, invariant bisnis apa yang harus tetap benar?”


3. Mental Model: Three Lifecycles, One Cost Evidence Graph

Gunakan mental model berikut:

Kunci desain:

  1. Cost evidence tidak boleh hilang.
    Setiap cost yang masuk ke asset, project, atau service harus punya sumber: invoice, receipt, inventory issue, labor, expense, atau adjustment.

  2. Lifecycle berbeda dari accounting treatment.
    Asset bisa operationally active tetapi belum capitalized. Project bisa operationally active tetapi financial status-nya WIP. Service order bisa closed secara operasional tetapi billing masih pending.

  3. State transition harus guarded by evidence.
    Tidak boleh dispose asset tanpa disposal reason, approval, asset status, accounting effect, dan traceability.

  4. Correction lebih penting daripada delete.
    ERP harus mengutamakan reversal, adjustment, reclassification, dan re-posting yang defensible.

  5. Reporting harus dapat menjawab “why”.
    Bukan hanya “asset value = X”, tapi “nilai ini berasal dari cost mana, depreciation mana, adjustment mana, dan approval siapa”.


4. Domain Boundary: Jangan Campur Semuanya ke Modul Asset

Salah satu anti-pattern ERP adalah membuat modul asset/project/service mengurus semua hal sendiri: inventory, procurement, billing, accounting, approval, dan reporting sekaligus. Ini menghasilkan coupling berat.

Boundary yang lebih sehat:

DomainOwnsDoes Not Own
Asset Managementasset master, asset lifecycle, location/custodian, maintenance association, depreciation intentGL balance sebagai source of truth, procurement invoice, inventory stock
Project Managementproject structure, task/WBS, project lifecycle, budget, cost allocation ruleAP invoice lifecycle, payroll, inventory balance
Service Managementservice request/order, SLA, technician assignment, work completion, entitlement decisioncustomer master, item master, stock ledger, AR receipt
Accountingjournal, posting, accounting period, balance, reconciliationphysical condition asset, technician schedule
Inventorystock movement, spare part availability, lot/serialservice closure decision, asset useful life
ProcurementPO/GRN/invoice matchingasset capitalization approval

Rule praktis:

Domain yang menciptakan real-world fact harus own event-nya; domain accounting hanya mengubah fact tersebut menjadi financial representation.

Contoh:

  • Service Management mencatat SparePartConsumed.
  • Inventory memvalidasi dan memposting StockIssued.
  • Accounting memposting expense/capitalization dari event yang sudah valid.
  • Service Management tidak langsung mengurangi stock balance dengan update SQL.

5. Asset Management: Apa yang Sebenarnya Dimodelkan?

Asset ERP memiliki dua sisi:

  1. Operational asset
    Sesuatu yang digunakan, dipindahkan, dirawat, diperiksa, dan dimiliki custodian.

  2. Financial fixed asset
    Sesuatu yang dikapitalisasi, disusutkan, di-revalue/impair, dan dilaporkan.

Keduanya tidak selalu 1:1.

Contoh:

RealitasOperational AssetFinancial Asset
Laptop murah yang langsung expensedyesno
Mesin produksi besaryesyes
Software license capitalizedmaybe no physical trackingyes
Spare component dalam mesinyes as componentmaybe part of parent asset
Building renovationmaybe project deliverableyes asset improvement

Desain yang baik tidak memaksa semua barang inventaris menjadi fixed asset.


6. Asset Master Model

Minimum conceptual model:

Important fields:

FieldWhy It Matters
asset_numberlegal/audit identity, often controlled sequence
asset_tagphysical label, may be changed/replaced
serial_numbermanufacturer identity, useful for warranty/recall
asset_class_iddrives accounting, maintenance, depreciation default
legal_entity_iddetermines ownership and accounting books
current_location_idoperational control and physical audit
current_custodian_idaccountability
operational_statususable, under repair, retired, lost, disposed
financial_statusnot_capitalized, capitalized, fully_depreciated, disposed
in_service_datedepreciation and maintenance scheduling trigger
versionoptimistic concurrency guard

7. Asset Lifecycle State Machine

Asset lifecycle perlu membedakan physical, operational, dan financial status. Untuk versi awal, buat state machine utama:

Transition guards:

TransitionRequired EvidenceBlock If
Received -> RegisteredGRN, asset class, legal entity, asset tag policyduplicate serial/tag
Registered -> InServicecommissioning date, custodian/location, capitalization decisionfuture locked period invalid
InService -> Transferredapproval, target org/location, custody acceptanceactive maintenance order not allowed
InService -> UnderMaintenancemaintenance order, technician/resource assignmentasset already under maintenance
InService -> Retiredretirement reason, approval, financial impact estimateopen service/project cost pending
Retired -> Disposeddisposal document, proceeds/scrap value, accounting posting requestaccounting period closed without adjustment path

Do not use a single string status without transition rule. State transition should be a command, not a raw update.


8. Java Aggregate Sketch: Asset Lifecycle

public final class Asset {
    private final AssetId id;
    private final AssetNumber number;
    private AssetOperationalStatus operationalStatus;
    private AssetFinancialStatus financialStatus;
    private LocationId currentLocation;
    private PartyId currentCustodian;
    private Instant inServiceAt;
    private long version;

    public AssetRegistered register(AssetRegistrationCommand command, AssetPolicy policy) {
        requireStatus(AssetOperationalStatus.RECEIVED);
        policy.assertTagAllowed(command.assetTag(), command.serialNumber());

        this.operationalStatus = AssetOperationalStatus.REGISTERED;

        return new AssetRegistered(
            id,
            command.assetTag(),
            command.serialNumber(),
            command.actorId(),
            command.evidenceId(),
            command.occurredAt()
        );
    }

    public AssetCommissioned commission(CommissionAssetCommand command, AccountingPeriodPolicy periodPolicy) {
        requireStatus(AssetOperationalStatus.REGISTERED);
        periodPolicy.assertOperationalDateAllowed(command.inServiceDate());

        this.operationalStatus = AssetOperationalStatus.IN_SERVICE;
        this.currentLocation = command.locationId();
        this.currentCustodian = command.custodianId();
        this.inServiceAt = command.occurredAt();

        return new AssetCommissioned(
            id,
            command.locationId(),
            command.custodianId(),
            command.inServiceDate(),
            command.actorId(),
            command.occurredAt()
        );
    }

    public AssetTransferInitiated transfer(TransferAssetCommand command, TransferPolicy policy) {
        requireStatus(AssetOperationalStatus.IN_SERVICE);
        policy.assertTransferAllowed(this, command.targetLocation(), command.targetCustodian());

        this.operationalStatus = AssetOperationalStatus.TRANSFER_PENDING;

        return new AssetTransferInitiated(
            id,
            currentLocation,
            command.targetLocation(),
            currentCustodian,
            command.targetCustodian(),
            command.reason(),
            command.actorId(),
            command.occurredAt()
        );
    }

    private void requireStatus(AssetOperationalStatus expected) {
        if (operationalStatus != expected) {
            throw new DomainRuleViolation("Asset must be " + expected + " but was " + operationalStatus);
        }
    }
}

Observe:

  • command membawa actor dan evidence;
  • policy memisahkan rule yang berubah;
  • aggregate tidak langsung posting GL;
  • method mengembalikan domain event;
  • transition invalid gagal sebelum state berubah.

9. Capitalization Boundary

Capitalization adalah keputusan bahwa cost tertentu menjadi nilai asset, bukan langsung expense.

Sources of capitalizable cost:

  • purchase invoice untuk asset;
  • freight/import cost;
  • installation service;
  • internal labor yang memenuhi policy;
  • project construction cost;
  • major improvement;
  • commissioning cost.

Sumber yang biasanya tidak langsung menjadi asset:

  • routine maintenance;
  • minor repair;
  • consumable spare part;
  • training umum;
  • operating expense setelah asset siap digunakan.

ERP harus mendukung policy tanpa hardcode berlebihan.

Rule penting:

Capitalization tidak boleh menghapus cost source. Ia hanya menghubungkan cost source ke asset valuation event.

Suggested table:

create table asset_capitalization_source (
    id uuid primary key,
    asset_id uuid not null,
    source_type varchar(40) not null,
    source_id uuid not null,
    source_line_id uuid,
    amount numeric(19, 4) not null,
    currency char(3) not null,
    classification varchar(40) not null,
    included_in_asset_value boolean not null,
    capitalization_request_id uuid,
    created_at timestamptz not null,
    unique (source_type, source_id, source_line_id, asset_id)
);

The unique key prevents the same AP invoice line from being capitalized twice into the same asset.


10. Depreciation: ERP Design Boundary

Depreciation has accounting specifics that depend on policy, jurisdiction, book, and standard. In ERP architecture, focus on the control model:

  1. asset has depreciation profile;
  2. profile creates schedule/projection;
  3. monthly run creates depreciation proposal;
  4. proposal is reviewed/approved;
  5. approved proposal creates posting request;
  6. posting result is linked back to asset;
  7. adjustment/reversal is explicit.

Key data:

EntityPurpose
asset_bookfinancial/tax/management book context
depreciation_profilemethod, useful life, residual value, start rule
depreciation_scheduleprojected lines, not necessarily posted
depreciation_runperiod-level calculation batch
depreciation_lineasset-period calculation result
asset_valuation_eventacquisition, capitalization, depreciation, impairment, disposal
posting_requestaccounting handoff

Important invariant:

For each asset book and period:
  posted depreciation must be derived from exactly one approved run line
  unless explicitly adjusted/reversed with evidence.

Never implement depreciation as a nightly job that directly inserts GL journals without proposal, approval, and traceability.


11. Asset Hierarchy and Component Replacement

Large assets are often hierarchies:

  • building -> floor -> HVAC -> compressor;
  • machine -> motor -> gearbox -> sensor;
  • vehicle -> engine -> battery -> tire set;
  • data center -> rack -> server -> storage component.

Asset hierarchy matters for:

  • maintenance planning;
  • warranty;
  • downtime impact;
  • component replacement;
  • capitalization of improvement;
  • disposal/retirement;
  • spare part compatibility;
  • traceability.

Anti-pattern:

parent_asset_id nullable on asset table, updated freely

Better:

asset_component_relation as effective-dated history

Because component membership changes over time.

Example component replacement lifecycle:

In Java, treat replacement as a service that coordinates:

  • asset aggregate;
  • inventory issue;
  • maintenance work order;
  • accounting classification;
  • warranty decision;
  • audit trail.

Do not let one aggregate own all of that.


12. Maintenance Management

Maintenance domain supports:

  1. preventive maintenance;
  2. corrective maintenance;
  3. predictive/condition-based maintenance;
  4. inspection;
  5. calibration;
  6. shutdown/overhaul;
  7. external service maintenance.

Core lifecycle:

Maintenance work order should capture:

AreaData
Identitymaintenance order number, type, priority
Targetasset, component, location
Triggerschedule, fault, inspection, service request, IoT condition
Plantask list, estimated labor, required spare parts, tools
Executiontechnician, time, readings, observations, attachments
Materialrequested, reserved, issued, returned spare parts
Costlabor, material, external service, overhead
Downtimeplanned/unplanned downtime interval
Resultcompleted, failed, rework, asset condition
Evidencechecklist, photo, signature, approval, audit log

13. Preventive Maintenance Scheduling

Preventive maintenance can be based on:

  • calendar interval: every 30 days;
  • meter interval: every 10,000 km;
  • usage hour: every 500 operating hours;
  • production count: every 100,000 units;
  • condition threshold: vibration > threshold;
  • regulatory inspection date.

Model preventive rule separately from generated work order.

Scheduling guard:

A preventive maintenance order should not be generated twice for the same asset, plan, trigger window, and due basis.

Suggested idempotency key:

PM:{assetId}:{planId}:{triggerRuleId}:{dueWindowStart}:{dueWindowEnd}

14. Project Management vs Project Accounting

ERP project domain has two distinct concerns:

  1. Project execution
    Scope, task, milestone, resource, schedule, deliverable.

  2. Project accounting
    Budget, committed cost, actual cost, WIP, billing, capitalization, profitability.

Do not collapse both into a single project table and a project_cost table.

Project structure:

WBS means Work Breakdown Structure. It is not only reporting hierarchy. It defines where cost and billing belong.


15. Project Cost Capture

Project cost can come from many modules:

SourceExampleControl Risk
ProcurementPO for subcontractorwrong WBS assignment
AP invoicevendor invoiceduplicate cost capture
Inventory issuematerial used in projectstock issued but project cost missing
Timesheetengineer hoursunapproved time included
Expense claimtravel/hotelwrong project/customer
Service orderimplementation/service workclosed order not billed
Journal adjustmentmanual correctionaudit weakness

The project module should receive cost events, not perform direct reads across all module tables for financial truth.

Cost event contract fields:

{
  "eventId": "uuid",
  "sourceSystem": "procurement",
  "sourceType": "AP_INVOICE_LINE",
  "sourceId": "uuid",
  "sourceLineId": "uuid",
  "projectId": "uuid",
  "wbsNodeId": "uuid",
  "costType": "MATERIAL|LABOR|SUBCONTRACT|EXPENSE|OVERHEAD",
  "amount": "1200000.00",
  "currency": "IDR",
  "costDate": "2026-06-30",
  "approvalStatus": "APPROVED",
  "accountingPeriod": "2026-06",
  "idempotencyKey": "APINV:...:LINE:...:PROJECT:..."
}

Invariant:

The same source line cannot be captured into project actual cost more than once
unless explicitly split by allocation rule and all splits sum to the original source amount.

16. Project Lifecycle State Machine

Important distinction:

Lifecycle StatusAccounting Status
Proposedno cost allowed except pre-sales if policy allows
Approvedcommitted cost allowed
Activeactual cost collection allowed
Closingnew cost blocked except approved late cost
Closedno cost/billing changes except adjustment process

A common ERP failure is allowing late AP invoices to post to closed projects with no reopening/reclassification workflow.

Better model:

Late cost for closed project -> exception queue -> approve re-open, accrue, expense elsewhere, or reject.

17. Project Budget and Commitment Control

Project ERP often needs budget control:

  • approved budget;
  • revised budget;
  • committed cost from PO;
  • actual cost from AP/inventory/time;
  • forecast cost to complete;
  • variance.

Budget invariant:

available_budget = approved_budget + approved_revision - committed_cost - actual_cost

But this formula is not always enough. Need semantics:

Cost TypeWhen Counted as CommitmentWhen Converted to Actual
PurchasePO approvedAP invoice matched / goods received depending policy
Materialreservation or issue requeststock issue posted
Laborplanned assignment maybe forecastapproved timesheet
Expenseclaim submitted maybe commitmentapproved expense
Subcontractsubcontract POvendor invoice/service acceptance

Concurrency issue:

Two users create PO lines at the same time against the last remaining project budget.

Solution options:

  1. pessimistic lock budget bucket;
  2. optimistic lock + retry;
  3. budget reservation ledger;
  4. asynchronous budget check with exception queue.

For large ERP, budget reservation ledger is usually more auditable:


18. Project Billing and Revenue Boundary

Project billing can be:

  • time and material;
  • fixed price milestone;
  • progress percentage;
  • cost plus;
  • retainer;
  • subscription-like service;
  • internal capitalization only;
  • grant/funding claim.

Do not hardcode billing into project task completion. Use billing policy.

Revenue recognition can be complex and depends on accounting policy. In this series, keep the ERP engineering boundary clear:

  • project produces billing/revenue events or requests;
  • accounting/revenue subsystem decides recognition/posting according to policy;
  • project stores linkage and status, not journal truth.

19. Service Management Domain

Service management can cover:

  • customer support service;
  • field service;
  • internal IT/facility service;
  • warranty repair;
  • installation;
  • preventive service contract;
  • maintenance order execution;
  • service billing.

Core service model:

Service order lifecycle:


20. SLA and Escalation

SLA must be computed from explicit clocks, not guessed from status text.

Common SLA metrics:

  • response time;
  • assignment time;
  • arrival time;
  • resolution time;
  • closure time;
  • parts wait time;
  • customer hold time;
  • internal hold time.

Use SLA event history:

create table service_sla_event (
    id uuid primary key,
    service_order_id uuid not null,
    event_type varchar(60) not null,
    event_time timestamptz not null,
    actor_id uuid,
    reason_code varchar(60),
    pause_clock boolean not null default false,
    resume_clock boolean not null default false,
    created_at timestamptz not null
);

SLA calculation should be explainable:

resolution_due_at = reported_at + SLA duration - approved pause durations within business calendar

Do not store only sla_breached = true. Store enough evidence to reconstruct why.


21. Warranty and Entitlement

Warranty/entitlement determines who pays.

Inputs:

  • customer;
  • asset/product serial;
  • installation date;
  • warranty start/end;
  • service contract;
  • failure category;
  • usage/meter reading;
  • excluded damage;
  • prior repairs;
  • part warranty;
  • labor coverage;
  • geography/channel.

Decision model:

Warranty decision should be versioned:

Entitlement decision = input facts + rule version + actor/engine + timestamp + outcome + override evidence

Why? Because warranty disputes happen after the service is completed.


22. Spare Part Usage in Service/Maintenance

Spare part usage touches Inventory and Service.

Bad design:

serviceOrder.setPartUsed(partId, qty);
itemRepository.decreaseStock(partId, qty);

Better design:

Important cases:

CaseDesign Response
part reserved but not usedrelease reservation
part issued but service cancelledreturn to stock or expense/write-off
part used under warrantycost to warranty accrual/cost center
part billable to customercreate billing request
serialized part replacedupdate asset/component genealogy
defective replaced part returnedreceive into quarantine/repairable stock

23. Labor Capture

Labor can come from:

  • timesheet;
  • field technician app;
  • work order start/stop;
  • manual supervisor entry;
  • external vendor service invoice.

Do not confuse attendance with chargeable labor.

Labor dimensions:

DimensionExamples
workeremployee, contractor, vendor technician
activityrepair, diagnosis, travel, installation, inspection
billabilitybillable, non-billable, warranty, internal
costingstandard rate, actual payroll cost, vendor rate
approvalself-entry, supervisor approval, customer sign-off
allocationproject, asset, service order, cost center

Labor capture event:

public record LaborCaptured(
    UUID eventId,
    UUID workerId,
    UUID serviceOrderId,
    UUID projectId,
    UUID assetId,
    String activityCode,
    BigDecimal hours,
    String billability,
    LocalDate workDate,
    UUID approvalId,
    String idempotencyKey
) {}

Invariant:

Chargeable labor cannot be billed or costed unless approved according to the configured authority rule.

24. Cross-Domain Cost Classification

Every cost related to asset/project/service needs classification:

A classification rule should consider:

  • source type;
  • asset class;
  • project type;
  • service type;
  • warranty decision;
  • amount threshold;
  • useful life extension;
  • legal entity;
  • accounting policy;
  • date/effective period;
  • manual override authority.

Design pattern:

Cost event -> classification proposal -> approval if high-risk -> posting/capitalization/billing request

25. Reporting Model

Operational reporting questions:

  • Which assets are in service, under maintenance, retired, disposed?
  • Which assets are overdue for preventive maintenance?
  • What is asset downtime by location/class?
  • Which projects exceed budget?
  • Which projects have committed cost but no actual invoice?
  • Which service orders breached SLA?
  • Which warranty claims are high cost?
  • Which spare parts are consumed most by asset class?

Financial reporting questions:

  • acquisition cost by asset class;
  • accumulated depreciation by book;
  • project WIP by customer/project;
  • project profitability;
  • maintenance cost by asset/location;
  • warranty cost by product/serial;
  • service revenue vs service cost.

Use separate read models:

Read ModelSource
asset_position_viewasset master + status/location history
asset_financial_summaryasset valuation events + GL posting result
maintenance_backlog_viewmaintenance order + SLA/schedule
project_cost_summaryproject ledger
project_budget_control_viewbudget + commitment + actual
service_sla_dashboardservice order + SLA event history
warranty_cost_viewentitlement + cost classification

Avoid reporting directly from mutable workflow tables for financial truth.


26. Concurrency and Consistency Risks

26.1 Asset Transfer Race

Problem:

  • user A transfers asset to Branch X;
  • user B transfers same asset to Branch Y;
  • both see status IN_SERVICE.

Controls:

  • optimistic lock on asset aggregate;
  • transfer pending status;
  • unique active transfer request per asset;
  • approval token tied to version;
  • custody acceptance step.

26.2 Maintenance vs Disposal Race

Problem:

  • maintenance starts;
  • finance disposes asset at same time.

Controls:

Asset disposal requires no open maintenance/service order unless override approval exists.

26.3 Project Budget Race

Problem:

  • concurrent commitments exceed budget.

Controls:

  • budget reservation ledger;
  • lock per budget bucket;
  • idempotent commitment command;
  • compensating release when PO cancelled.

26.4 Service Closure Race

Problem:

  • technician closes order while inventory issue is still pending.

Controls:

Service order cannot close unless required cost/material/labor events are either posted, waived, or explicitly marked not required.

27. Failure Modes and Recovery Playbooks

FailureSymptomRoot CauseRecovery
Asset capitalized twiceasset value too highduplicate invoice line linkagereverse duplicate capitalization event
Depreciation missingperiod close mismatchbatch failed after partial processingrerun idempotent depreciation proposal
Asset disposed but still activeoperational/financial status split not enforceddisposal posted without operational transitioncorrective transition + audit note
Project actual cost duplicatedsame source captured twicemissing idempotency keyreversal cost transaction + unique constraint
Late cost on closed projectAP invoice arrives after closureno late-cost workflowexception queue: reopen/accrue/expense/reject
Service closed without parts costasync inventory issue failedclosure did not check pending eventsreopen or cost adjustment workflow
Warranty incorrectly grantedrule input wrongstale install date/customer entitlemententitlement correction + billing/cost reclassification
PM orders duplicatedschedule job rerunno due-window idempotencycancel duplicate + add idempotency key
Component genealogy brokenreplaced part not linkedupdate only free-text notescorrective component history entry

Recovery principle:

Never fix ERP financial/operational data with silent SQL unless it is part of a controlled data repair with evidence, approval, and replay-safe correction.


28. API Design Sketch

Command-oriented APIs:

POST /assets/{assetId}/register
POST /assets/{assetId}/commission
POST /assets/{assetId}/transfer-requests
POST /assets/{assetId}/retirement-requests
POST /assets/{assetId}/disposal-requests

POST /maintenance-orders
POST /maintenance-orders/{id}/release
POST /maintenance-orders/{id}/start
POST /maintenance-orders/{id}/complete
POST /maintenance-orders/{id}/close

POST /projects
POST /projects/{id}/approve
POST /projects/{id}/activate
POST /projects/{id}/cost-events
POST /projects/{id}/billing-requests
POST /projects/{id}/close

POST /service-requests
POST /service-orders/{id}/schedule
POST /service-orders/{id}/dispatch
POST /service-orders/{id}/record-labor
POST /service-orders/{id}/request-part
POST /service-orders/{id}/resolve
POST /service-orders/{id}/close

Avoid APIs like:

PATCH /assets/{id} { "status": "DISPOSED" }
PATCH /projects/{id} { "actualCost": 1000000 }
PATCH /service-orders/{id} { "slaBreached": false }

Because those bypass business transition and evidence.


29. Java Application Service Pattern

@Transactional
public DisposeAssetResult dispose(DisposeAssetCommand command) {
    IdempotencyToken token = idempotency.start(command.idempotencyKey());

    Asset asset = assetRepository.getForUpdate(command.assetId());
    OpenWorkCheck openWork = assetWorkQuery.checkOpenWork(command.assetId());
    AccountingPeriod period = periodService.getPeriod(command.disposalDate());

    AssetDisposed event = asset.dispose(command, openWork, period);

    assetRepository.save(asset);
    assetEventRepository.append(event);

    postingOutbox.add(PostingRequest.fromAssetDisposal(event));
    auditTrail.record(command.actor(), event, command.evidence());

    idempotency.complete(token, event.eventId());
    return new DisposeAssetResult(event.eventId(), asset.status());
}

Key points:

  • lock only what must be serialized;
  • write domain event and outbox in same DB transaction;
  • external GL/integration happens after commit;
  • command is idempotent;
  • audit is explicit;
  • business query checks open maintenance/service/project link.

30. Database Constraints Worth Having

Application rules are not enough. Add structural constraints:

-- Only one active component parent relation for a child asset at a time, implemented using exclusion/index strategy where supported.

-- Same source line cannot be capitalized twice into same asset.
alter table asset_capitalization_source
add constraint uq_asset_cap_source unique (source_type, source_id, source_line_id, asset_id);

-- Same source line cannot be captured twice as project actual cost unless split allocation id is used.
create unique index uq_project_cost_source
on project_cost_transaction(source_type, source_id, source_line_id, project_id, wbs_node_id)
where reversal_of_id is null;

-- One active transfer request per asset.
create unique index uq_active_asset_transfer
on asset_transfer_request(asset_id)
where status in ('REQUESTED', 'APPROVED', 'PENDING_ACCEPTANCE');

-- Prevent duplicate preventive maintenance generation.
create unique index uq_pm_due_window
on maintenance_order(asset_id, maintenance_plan_id, trigger_rule_id, due_window_start, due_window_end)
where source = 'PREVENTIVE_MAINTENANCE';

Database constraints should enforce high-value invariants that must survive application bugs, retries, and concurrent requests.


31. Observability

Metrics to expose:

MetricMeaning
asset_capitalization_pending_countcost waiting capitalization decision
asset_depreciation_run_failed_countfailed depreciation batches
asset_open_maintenance_by_prioritymaintenance backlog
project_budget_exceeded_countcontrol failure/override volume
project_late_cost_exception_countlate cost pressure
service_sla_breach_countservice health
service_parts_wait_durationinventory impact on service
warranty_override_raterule quality/abuse signal
cost_classification_manual_override_ratepolicy ambiguity signal

Logs should include:

  • asset/project/service id;
  • legal entity;
  • actor;
  • command id;
  • idempotency key;
  • source document;
  • transition from/to;
  • reason code;
  • posting request id.

A support engineer should be able to answer:

“Why did this project cost appear in June even though the project was closed in May?”

without opening production database manually.


32. Design Review Checklist

Asset

  • Does asset model separate operational status and financial status?
  • Are asset transfers effective-dated and auditable?
  • Can capitalization trace back to source cost lines?
  • Is depreciation proposal/review/posting separated?
  • Is disposal blocked by open maintenance/service/cost where required?
  • Is component replacement history preserved?

Project

  • Does WBS drive cost collection and billing policy?
  • Are committed and actual costs separated?
  • Is budget control concurrency-safe?
  • Can late costs after close be handled?
  • Can source cost be traced back to AP/inventory/time/expense?
  • Is project accounting status separate from project lifecycle status?

Service

  • Are SLA clocks event-based and explainable?
  • Is warranty decision versioned and auditable?
  • Does spare part usage integrate with inventory via reservation/issue?
  • Does closure check material/labor/cost completeness?
  • Can service order be reopened with evidence?
  • Are billable, warranty, and internal costs clearly classified?

33. Practice Drill: 20-Hour Applied Slice

Build a thin vertical slice:

  1. Register an asset from a goods receipt.
  2. Commission asset into service.
  3. Generate preventive maintenance order.
  4. Reserve and issue spare part.
  5. Capture technician labor.
  6. Complete maintenance.
  7. Classify cost as repair expense.
  8. Create posting request.
  9. Show asset maintenance cost report.
  10. Simulate duplicate maintenance completion retry.
  11. Simulate service closure while inventory issue pending.
  12. Simulate asset disposal while maintenance is open.

Acceptance criteria:

  • duplicate commands are idempotent;
  • invalid lifecycle transition is rejected;
  • all cost has source evidence;
  • maintenance cannot silently bypass inventory;
  • audit log can reconstruct lifecycle;
  • reporting read model matches event history.

34. Mental Compression

Remember this:

Asset = physical/control lifecycle + financial valuation lifecycle.
Project = execution lifecycle + cost/budget/billing lifecycle.
Service = request/work lifecycle + entitlement/SLA/cost lifecycle.

The top 1% ERP engineer does not ask only:

“What table stores this?”

They ask:

“What fact happened, who is allowed to assert it, what invariant must remain true, what other domains need to know, what evidence proves it, and how do we correct it later without destroying auditability?”


35. Source Notes

  • Jakarta Persistence 3.2 defines standard persistence and object/relational mapping facilities for Java/Jakarta EE applications and is relevant as the persistence baseline for enterprise ERP modules.
  • Jakarta EE 11 includes updated platform specifications relevant to enterprise Java systems, including Persistence, Transactions, Concurrency, Validation, Messaging, Batch, Authorization, and Jakarta Data.
  • Spring Boot 4.1 requires Java 17 and is compatible with Java versions up to Java 26, making it relevant for modern Java ERP service implementation.
  • Apache OFBiz is a Java-based open-source ERP reference that includes modules such as Accounting, Fixed Assets, Order, Manufacturing, Facility/Warehouse, and related enterprise application areas.
  • IAS 16 establishes principles for recognizing property, plant and equipment as assets and for measuring carrying amounts, depreciation charges, and impairment losses. This part does not prescribe accounting policy; it models the ERP control boundaries around asset valuation and posting.
Lesson Recap

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