Learn Enterprise Cpq Oms Glassfish Camunda8 Part 011 Asset And Subscription Model
title: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform - Part 011 description: Mendesain installed base, asset, subscription, product instance, service instance, dan relationship-nya dengan order management untuk enterprise CPQ/OMS yang mendukung add, modify, disconnect, move, renewal, amendment, audit, dan long-lived customer ownership. series: learn-enterprise-cpq-oms-glassfish-camunda8 seriesTitle: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform order: 11 partTitle: Asset and Subscription Model tags:
- java
- microservices
- cpq
- oms
- asset-management
- subscription
- installed-base
- order-management
- postgresql
- mybatis
- kafka
- camunda-8
- enterprise-architecture date: 2026-07-02
Part 011 — Asset and Subscription Model
Di Part 010 kita memodelkan order sebagai execution commitment.
Sekarang kita masuk ke satu domain yang sering disepelekan dalam sistem CPQ/OMS, padahal justru domain ini yang membuat enterprise order management menjadi sulit: installed base, asset, dan subscription.
Jika sistem hanya menjual satu kali lalu selesai, asset model mungkin sederhana.
Tetapi enterprise CPQ/OMS biasanya tidak hanya menjual sesuatu. Ia mengelola hubungan panjang antara customer dan produk/layanan yang sudah aktif.
Customer bisa:
- membeli produk baru;
- menambah add-on;
- mengubah paket;
- menaikkan bandwidth;
- menurunkan tier;
- memindahkan lokasi layanan;
- mengganti perangkat;
- memperbarui kontrak;
- menghentikan layanan sebagian;
- menghentikan layanan penuh;
- melakukan amendment ketika order lama belum selesai;
- meminta koreksi karena fulfillment external tidak sesuai.
Semua ini tidak bisa dimodelkan hanya dengan orders dan order_items.
Kita butuh installed base.
1. Target Part Ini
Part ini memaku model untuk:
- customer asset;
- product instance;
- service instance;
- resource instance;
- subscription;
- agreement reference;
- asset relationship;
- asset versioning;
- installed base snapshot;
- order action terhadap asset;
- add/modify/disconnect/move/renew flows;
- asset lifecycle;
- subscription lifecycle;
- PostgreSQL schema awal;
- MyBatis persistence direction;
- Kafka event model;
- Camunda boundary;
- failure modes;
- testing strategy.
Kita belum membangun full asset service implementation. Itu akan muncul bertahap ketika order, decomposition, fulfillment, dan integration adapter mulai dibangun.
Di part ini kita membangun mental model dan kontrak domainnya.
2. Installed Base: Apa Yang Sebenarnya Disimpan?
Installed base adalah representasi sistem terhadap produk/layanan yang sudah dimiliki, digunakan, aktif, suspended, pending change, atau pernah dimiliki customer.
Installed base bukan katalog.
Installed base bukan order history.
Installed base bukan billing account.
Installed base adalah keadaan aktual atau near-actual dari relationship customer terhadap product/service instance.
Contoh sederhana:
Customer: PT Contoh Digital
Location: Jakarta HQ
Active installed base:
- Business Fiber 1Gbps
- Static IP /29
- Managed Router
- SLA Gold
- Cloud Backup 5TB
Jika customer ingin upgrade Business Fiber 1Gbps menjadi 2Gbps, CPQ tidak boleh hanya melihat catalog. CPQ harus tahu customer sekarang sudah punya apa.
Jika customer ingin disconnect Managed Router, OMS harus tahu router itu child dari Business Fiber instance, bukan produk lepas.
Jika customer ingin pindah lokasi, OMS harus tahu service instance mana yang dipindahkan, appointment mana yang diperlukan, dan fulfillment task apa yang harus dibuat.
3. Empat Level Yang Harus Dibedakan
Dalam enterprise OMS, kita perlu membedakan minimal empat level:
Commercial Product Instance
Service Instance
Resource Instance
Subscription / Commercial Commitment
Masing-masing punya alasan hidup yang berbeda.
3.1 Product Instance
Product instance adalah representasi komersial dari produk yang customer miliki.
Contoh:
Business Fiber 1Gbps
Static IP /29
SLA Gold
Managed Router
Product instance menjawab:
customer membeli atau memiliki produk komersial apa?
Ia biasanya berasal dari product_offering dan product_specification pada catalog.
3.2 Service Instance
Service instance adalah representasi layanan teknis yang dipenuhi oleh operasi.
Contoh:
Internet Access Service
IP Allocation Service
Managed CPE Service
SLA Monitoring Service
Service instance menjawab:
layanan teknis apa yang harus aktif agar produk komersial ini benar-benar terpenuhi?
Product instance bisa memetakan ke satu atau beberapa service instance.
3.3 Resource Instance
Resource instance adalah representasi resource fisik/logis yang dialokasikan.
Contoh:
ONT serial number
Router serial number
Port ID
VLAN ID
IP block
SIM card
Phone number
Cloud quota bucket
Resource instance menjawab:
resource apa yang dikonsumsi atau dikunci untuk memenuhi service?
OMS sering tidak menjadi owner resource inventory. Namun OMS tetap perlu reference agar order traceable.
3.4 Subscription
Subscription adalah commercial continuity contract.
Contoh:
Monthly Business Fiber 1Gbps subscription
Contract term: 24 months
Billing cycle: monthly
Start date: 2026-07-02
End date: 2028-07-01
Auto-renew: true
Subscription menjawab:
komitmen berulang apa yang berjalan antara customer dan business?
Subscription bukan sekadar product instance.
Product instance bisa aktif tanpa subscription dalam model tertentu, misalnya one-time hardware sale.
Subscription bisa mengikat banyak product instances dalam satu commercial commitment.
4. Jangan Campur Asset Dengan Catalog
Catalog menjawab:
Apa yang bisa dijual?
Installed base menjawab:
Apa yang sudah dimiliki atau sedang digunakan customer?
Order menjawab:
Perubahan apa yang diminta dan harus dieksekusi?
Billing menjawab:
Apa yang harus ditagihkan?
Provisioning menjawab:
Apa yang harus dikonfigurasi di network/system teknis?
Jika asset dicampur dengan catalog, maka product offering yang berubah akan terlihat seolah-olah semua customer asset ikut berubah.
Ini berbahaya.
Customer yang membeli produk lama harus tetap bisa direkonstruksi berdasarkan versi yang berlaku saat purchase atau activation.
Karena itu product instance harus menyimpan snapshot referensi catalog, bukan hanya pointer tipis.
5. Core Mental Model
Kita dapat menggambarkan hubungan utama seperti ini:
Dari diagram ini, terlihat bahwa order item bukan owner penuh product instance. Order item adalah penyebab perubahan.
Asset menyimpan keadaan setelah perubahan diterapkan.
6. Entity Utama
6.1 Customer Asset
CustomerAsset adalah container installed base milik customer/account.
Ia bisa berupa aggregate root tersendiri atau projection dari product instances.
Untuk sistem yang besar, lebih aman menganggap customer asset sebagai read/query boundary, sementara product instance menjadi entity utama untuk mutation.
Minimal fields:
asset_id
customer_id
account_id
root_product_instance_id
status
created_at
updated_at
Namun jangan tergoda membuat satu table customer_asset yang menyimpan semua detail dalam JSON tanpa struktur.
JSON boleh dipakai untuk snapshot/configuration, tetapi lifecycle, relationship, dan status harus queryable.
6.2 Product Instance
Product instance adalah commercial instance.
Fields penting:
product_instance_id
customer_id
account_id
subscription_id
product_offering_id
product_offering_version
product_specification_id
product_specification_version
name
status
action_origin_order_id
action_origin_order_item_id
activation_date
termination_date
configuration_snapshot
price_snapshot
catalog_snapshot
version
created_at
updated_at
configuration_snapshot, price_snapshot, dan catalog_snapshot penting untuk historical reconstruction.
6.3 Service Instance
Service instance adalah technical service representation.
Fields penting:
service_instance_id
product_instance_id
service_specification_id
service_specification_version
status
external_service_id
activation_date
termination_date
technical_parameters
version
created_at
updated_at
external_service_id bisa menunjuk ke service inventory atau provisioning system.
6.4 Resource Instance
Resource instance adalah resource binding.
Fields penting:
resource_instance_id
service_instance_id
resource_type
resource_id
external_resource_id
status
allocation_date
release_date
attributes
created_at
updated_at
Resource instance dalam OMS biasanya bukan authoritative source untuk resource inventory. Ia menyimpan binding/reference untuk traceability.
6.5 Subscription
Subscription adalah commercial recurring relationship.
Fields penting:
subscription_id
customer_id
account_id
agreement_id
status
start_date
end_date
commitment_start_date
commitment_end_date
billing_account_id
billing_cycle
auto_renew
renewal_policy
created_at
updated_at
version
Subscription perlu status sendiri karena product instance dan subscription bisa tidak selalu berubah bersamaan.
Contoh:
- subscription active, satu child product suspended;
- product still active, subscription pending renewal;
- subscription terminated, product instance entering disconnect workflow;
- subscription migrated ke agreement baru.
7. Lifecycle Asset
Asset lifecycle harus lebih stabil daripada fulfillment task lifecycle.
Contoh status product instance:
PENDING_ACTIVATION
ACTIVE
PENDING_MODIFICATION
SUSPENDED
PENDING_DISCONNECT
DISCONNECTED
CANCELLED
ERROR
Jangan terlalu banyak status teknis di product instance.
Status seperti WAITING_FOR_ROUTER_DELIVERY, PROVISIONING_NETWORK_PROFILE, atau TECHNICIAN_ASSIGNED adalah fulfillment task/workflow state, bukan product instance state.
7.1 Product Instance State Machine
7.2 Subscription State Machine
8. Asset Relationship Model
Enterprise products jarang berdiri sendiri.
Contoh:
Business Fiber 1Gbps
├── Static IP /29
├── Managed Router
└── SLA Gold
Atau:
Enterprise Mobile Plan
├── SIM Card
├── Data Package
├── Roaming Add-on
└── Device Installment
Kita butuh relationship antar product instances.
Fields:
relationship_id
source_product_instance_id
target_product_instance_id
relationship_type
valid_from
valid_to
created_by_order_item_id
created_at
Relationship type contoh:
BUNDLES
DEPENDS_ON
REQUIRES
ADDON_OF
REPLACES
MIGRATED_FROM
PARENT_OF
8.1 Relationship Direction
Direction harus konsisten.
Saya sarankan:
source = parent / dependent owner / newer object
target = child / dependency / previous object
Contoh:
Business Fiber source --PARENT_OF--> Static IP target
SLA Gold source --ADDON_OF--> Business Fiber target
Business Fiber 2Gbps source --REPLACES--> Business Fiber 1Gbps target
Namun untuk relationship ADDON_OF, secara natural source bisa child dan target parent. Yang penting aturan ini eksplisit dan dites.
Jangan biarkan developer berbeda membuat arah relationship berbeda.
9. Asset Versioning
Installed base adalah long-lived data.
Kita perlu tahu:
- keadaan sekarang;
- perubahan apa yang pernah terjadi;
- order mana yang mengubahnya;
- configuration sebelum dan sesudah;
- price sebelum dan sesudah;
- kapan perubahan berlaku;
- apakah perubahan sudah efektif secara technical/billing/commercial.
Ada dua pendekatan umum.
9.1 Current Table + History Table
Current table menyimpan state terbaru.
History table menyimpan setiap versi.
product_instance
product_instance_history
Keuntungan:
- query state sekarang cepat;
- history bisa dipakai audit;
- cocok dengan MyBatis explicit SQL;
- lebih mudah untuk operational dashboard.
Kekurangan:
- harus disiplin menulis history setiap mutation;
- replay domain event tidak otomatis membangun state;
- repair script harus hati-hati.
9.2 Event-Sourced Asset
Semua perubahan asset disimpan sebagai event dan state dibangun dari replay.
Keuntungan:
- audit natural;
- temporal reconstruction kuat;
- event history lengkap.
Kekurangan:
- implementation jauh lebih sulit;
- query operational butuh projection;
- schema evolution event menjadi kompleks;
- repair human operator lebih sulit.
Untuk seri ini kita pakai current table + history table + domain events + audit log.
Ini lebih realistis untuk enterprise Java microservices yang memakai PostgreSQL, MyBatis, Kafka, dan operational dashboard.
10. Effective Date vs Transaction Date
Asset management selalu punya dua jenis waktu.
10.1 Transaction Time
Kapan sistem mencatat perubahan.
Contoh:
2026-07-02 10:30:00 order completed in OMS
10.2 Effective Time
Kapan perubahan dianggap berlaku secara bisnis/teknis.
Contoh:
2026-07-01 00:00:00 subscription renewal effective
Atau:
2026-07-05 09:00:00 service activated by provisioning system
Jangan hanya punya created_at dan updated_at.
Untuk asset dan subscription, kita butuh fields seperti:
valid_from
valid_to
effective_from
effective_to
activation_date
termination_date
Nama harus jelas. Jangan mencampur valid_from dan effective_from tanpa definisi.
Rekomendasi seri ini:
created_at: kapan row dibuat di sistem;updated_at: kapan row terakhir diubah;activation_date: kapan instance aktif secara layanan;termination_date: kapan instance berakhir;effective_from: kapan versi ini berlaku secara bisnis;effective_to: kapan versi ini tidak lagi berlaku.
11. Order Action Terhadap Asset
Order item harus menyatakan action terhadap installed base.
Action umum:
ADD
MODIFY
DISCONNECT
SUSPEND
RESUME
MOVE
REPLACE
RENEW
11.1 ADD
Membuat product instance baru.
Input:
product_offering_id
configuration_snapshot
price_snapshot
customer/account context
Output:
new product_instance_id
maybe new subscription_id
service/resource instances after fulfillment
11.2 MODIFY
Mengubah existing product instance.
Input:
target_product_instance_id
current asset snapshot
requested configuration delta
requested price delta
Output:
same product_instance_id with new version
maybe new service/resource changes
history record
Contoh:
Upgrade bandwidth from 1Gbps to 2Gbps
Add Static IP /29
Remove SLA Gold
11.3 DISCONNECT
Mengakhiri product instance.
Input:
target_product_instance_id
disconnect reason
requested termination date
Output:
product_instance status DISCONNECTED
termination_date set
child asset impact handled
resource release triggered
billing stop trigger emitted
11.4 MOVE
Memindahkan service/product ke lokasi baru.
MOVE tidak selalu sama dengan MODIFY.
MOVE bisa melibatkan:
- disconnect technical service di lokasi lama;
- reserve resource di lokasi baru;
- appointment baru;
- keep same commercial subscription;
- change service instance;
- change resource binding;
- maintain customer product continuity.
11.5 REPLACE
Mengganti product instance lama dengan instance baru.
Contoh:
Legacy Fiber 500Mbps replaced by Business Fiber 1Gbps
REPLACE sering lebih aman daripada MODIFY jika product specification berubah besar.
Hasilnya:
new product_instance_id --REPLACES--> old product_instance_id
old product_instance_id status DISCONNECTED or MIGRATED
11.6 RENEW
Memperpanjang subscription/agreement tanpa selalu mengubah product instance.
Renewal sebaiknya memutasi subscription dan agreement reference, bukan membuat product instance baru kecuali ada perubahan product.
12. Installed Base Snapshot Untuk CPQ
CPQ butuh installed base snapshot ketika melakukan configure/price/quote.
Jangan biarkan CPQ membaca live asset berkali-kali selama quote sedang berjalan tanpa snapshot.
Masalahnya:
- asset bisa berubah oleh order lain;
- quote bisa menjadi tidak deterministik;
- price eligibility bisa berubah diam-diam;
- approval yang sudah diberikan bisa tidak sesuai dengan kondisi awal;
- quote-to-order conversion bisa gagal karena state asset berubah.
Karena itu quote perlu menyimpan snapshot:
installed_base_snapshot_id
snapshot_time
customer_id
account_id
asset_versions[]
subscription_versions[]
Ketika quote dikonversi menjadi order, sistem harus melakukan stale check.
If target asset version != version used during quote,
then conversion must fail or require requote.
13. Concurrency Problem: Two Orders Target The Same Asset
Contoh:
- Order A: upgrade Business Fiber dari 1Gbps ke 2Gbps.
- Order B: disconnect Business Fiber.
Keduanya menargetkan product instance yang sama.
Jika berjalan bersamaan tanpa guard, hasilnya bisa absurd:
service upgraded after disconnect completed
billing started for product that is already terminated
resource reserved for inactive service
Kita butuh asset-level concurrency control.
13.1 Optimistic Locking
Product instance punya version.
Command membawa expected_version.
MODIFY product_instance_id=PI-123 expected_version=7
Update hanya berhasil jika current version masih 7.
13.2 Pending Change Lock
Product instance punya status PENDING_MODIFICATION atau PENDING_DISCONNECT.
Order baru yang menarget asset tersebut harus dievaluasi:
- reject;
- queue;
- merge;
- require manual review;
- allow only compatible action.
Untuk seri ini default-nya: reject conflicting active change, kecuali secara eksplisit didesain sebagai supplemental order.
13.3 Asset Operation Table
Kita dapat menambah table:
asset_operation_lock
Fields:
lock_id
product_instance_id
order_id
order_item_id
operation_type
status
created_at
released_at
Ini bukan distributed lock Redis. Ini transactional business lock di PostgreSQL.
Redis lock bisa membantu runtime short-lived coordination, tetapi source of truth conflict harus tetap di database.
14. PostgreSQL Schema Awal
Schema ini belum final, tetapi cukup untuk fondasi.
create table subscription (
subscription_id uuid primary key,
customer_id uuid not null,
account_id uuid not null,
agreement_id uuid null,
billing_account_id uuid null,
status varchar(40) not null,
start_date timestamptz null,
end_date timestamptz null,
commitment_start_date timestamptz null,
commitment_end_date timestamptz null,
billing_cycle varchar(40) null,
auto_renew boolean not null default false,
renewal_policy jsonb not null default '{}'::jsonb,
version bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table product_instance (
product_instance_id uuid primary key,
customer_id uuid not null,
account_id uuid not null,
subscription_id uuid null references subscription(subscription_id),
product_offering_id uuid not null,
product_offering_version varchar(40) not null,
product_specification_id uuid not null,
product_specification_version varchar(40) not null,
name varchar(300) not null,
status varchar(40) not null,
action_origin_order_id uuid null,
action_origin_order_item_id uuid null,
activation_date timestamptz null,
termination_date timestamptz null,
effective_from timestamptz null,
effective_to timestamptz null,
catalog_snapshot jsonb not null,
configuration_snapshot jsonb not null,
price_snapshot jsonb not null,
version bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index idx_product_instance_customer
on product_instance(customer_id, account_id, status);
create index idx_product_instance_subscription
on product_instance(subscription_id);
create table product_instance_relationship (
relationship_id uuid primary key,
source_product_instance_id uuid not null references product_instance(product_instance_id),
target_product_instance_id uuid not null references product_instance(product_instance_id),
relationship_type varchar(40) not null,
valid_from timestamptz null,
valid_to timestamptz null,
created_by_order_item_id uuid null,
created_at timestamptz not null default now(),
constraint uq_product_instance_relationship unique (
source_product_instance_id,
target_product_instance_id,
relationship_type,
valid_from
)
);
create table service_instance (
service_instance_id uuid primary key,
product_instance_id uuid not null references product_instance(product_instance_id),
service_specification_id uuid not null,
service_specification_version varchar(40) not null,
status varchar(40) not null,
external_service_id varchar(120) null,
activation_date timestamptz null,
termination_date timestamptz null,
technical_parameters jsonb not null default '{}'::jsonb,
version bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index idx_service_instance_product
on service_instance(product_instance_id, status);
create table resource_instance (
resource_instance_id uuid primary key,
service_instance_id uuid not null references service_instance(service_instance_id),
resource_type varchar(80) not null,
resource_id varchar(160) null,
external_resource_id varchar(160) null,
status varchar(40) not null,
allocation_date timestamptz null,
release_date timestamptz null,
attributes jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index idx_resource_instance_service
on resource_instance(service_instance_id, status);
14.1 History Tables
create table product_instance_history (
history_id uuid primary key,
product_instance_id uuid not null,
version bigint not null,
change_type varchar(40) not null,
changed_by_order_id uuid null,
changed_by_order_item_id uuid null,
before_state jsonb null,
after_state jsonb not null,
changed_at timestamptz not null default now(),
unique(product_instance_id, version)
);
create table subscription_history (
history_id uuid primary key,
subscription_id uuid not null,
version bigint not null,
change_type varchar(40) not null,
changed_by_order_id uuid null,
changed_by_order_item_id uuid null,
before_state jsonb null,
after_state jsonb not null,
changed_at timestamptz not null default now(),
unique(subscription_id, version)
);
14.2 Asset Operation Guard
create table asset_operation_guard (
guard_id uuid primary key,
product_instance_id uuid not null references product_instance(product_instance_id),
order_id uuid not null,
order_item_id uuid not null,
operation_type varchar(40) not null,
status varchar(40) not null,
created_at timestamptz not null default now(),
released_at timestamptz null
);
create unique index uq_asset_operation_guard_active
on asset_operation_guard(product_instance_id)
where status in ('ACTIVE', 'PENDING_RELEASE');
Partial unique index ini membantu memastikan satu asset tidak diproses oleh dua operation aktif sekaligus.
15. MyBatis Mapper Direction
Kita tidak akan menyembunyikan mutation penting di ORM magic.
Untuk asset, explicit SQL lebih mudah diaudit.
15.1 ProductInstanceMapper
public interface ProductInstanceMapper {
ProductInstanceRecord findById(UUID productInstanceId);
List<ProductInstanceRecord> findActiveByCustomerAccount(
@Param("customerId") UUID customerId,
@Param("accountId") UUID accountId
);
int insert(ProductInstanceRecord record);
int updateWithOptimisticLock(
@Param("record") ProductInstanceRecord record,
@Param("expectedVersion") long expectedVersion
);
int markPendingModification(
@Param("productInstanceId") UUID productInstanceId,
@Param("orderId") UUID orderId,
@Param("orderItemId") UUID orderItemId,
@Param("expectedVersion") long expectedVersion
);
}
15.2 Optimistic Update SQL
<update id="updateWithOptimisticLock">
update product_instance
set
status = #{record.status},
catalog_snapshot = #{record.catalogSnapshot, typeHandler=JsonbTypeHandler},
configuration_snapshot = #{record.configurationSnapshot, typeHandler=JsonbTypeHandler},
price_snapshot = #{record.priceSnapshot, typeHandler=JsonbTypeHandler},
activation_date = #{record.activationDate},
termination_date = #{record.terminationDate},
effective_from = #{record.effectiveFrom},
effective_to = #{record.effectiveTo},
version = version + 1,
updated_at = now()
where product_instance_id = #{record.productInstanceId}
and version = #{expectedVersion}
</update>
Jika return count 0, application service harus melempar ConcurrentAssetModificationException.
Jangan diam-diam reload dan overwrite.
16. Application Service Boundary
Mutasi asset harus terjadi melalui application service yang eksplisit.
Contoh:
public final class AssetApplicationService {
private final ProductInstanceRepository productInstances;
private final SubscriptionRepository subscriptions;
private final AssetOperationGuardRepository guards;
private final OutboxRepository outbox;
private final Clock clock;
public ProductInstanceId startModification(StartAssetModificationCommand command) {
ProductInstance current = productInstances.get(command.productInstanceId());
current.assertVersion(command.expectedVersion());
current.assertCanStartModification(command.operationType());
guards.acquire(
command.productInstanceId(),
command.orderId(),
command.orderItemId(),
command.operationType()
);
ProductInstance pending = current.startPendingModification(
command.orderId(),
command.orderItemId(),
clock.instant()
);
productInstances.save(pending, current.version());
outbox.append(ProductInstanceChangeStarted.from(pending));
return pending.id();
}
}
Perhatikan urutan:
- load current state;
- validate version;
- validate status;
- acquire business guard;
- mutate domain object;
- persist with optimistic lock;
- write outbox in same DB transaction.
17. Kafka Event Model
Asset events harus jelas membedakan event internal domain dan integration event.
Contoh domain events:
ProductInstanceCreated
ProductInstanceActivationPending
ProductInstanceActivated
ProductInstanceModificationStarted
ProductInstanceModified
ProductInstanceDisconnectStarted
ProductInstanceDisconnected
SubscriptionCreated
SubscriptionActivated
SubscriptionRenewed
SubscriptionTerminated
Contoh integration events:
customer.asset.product-instance.activated.v1
customer.asset.product-instance.modified.v1
customer.asset.product-instance.disconnected.v1
customer.subscription.activated.v1
customer.subscription.terminated.v1
17.1 Event Payload Minimal
{
"eventId": "9afc4c9e-4a9f-40d4-8b4e-68a9c2e6f5f0",
"eventType": "ProductInstanceModified",
"eventVersion": 1,
"occurredAt": "2026-07-02T10:30:00Z",
"correlationId": "corr-123",
"causationId": "order-item-456",
"customerId": "...",
"accountId": "...",
"productInstanceId": "...",
"subscriptionId": "...",
"previousVersion": 7,
"newVersion": 8,
"status": "ACTIVE",
"changeSummary": {
"operation": "MODIFY",
"changedCharacteristics": ["bandwidth"],
"changedPrices": ["monthlyRecurringCharge"]
}
}
Jangan mengirim semua snapshot besar ke semua consumer kecuali memang diperlukan.
Untuk payload besar, kirim reference dan sediakan query API.
18. Camunda Boundary
Camunda/Zeebe menjalankan orchestration.
Asset service tetap owner installed base.
Workflow boleh memanggil command seperti:
StartProductActivation
CompleteProductActivation
StartProductModification
CompleteProductModification
StartProductDisconnect
CompleteProductDisconnect
Workflow tidak boleh langsung update table product_instance.
Jika workflow incident terjadi, asset state tidak boleh berubah separuh tanpa domain command yang tercatat.
19. Redis Boundary
Redis boleh dipakai untuk:
- cache installed base snapshot untuk read-heavy CPQ;
- cache active product list per account;
- short-lived lookup hasil query;
- rate limiting asset-heavy APIs;
- temporary workflow correlation lookup.
Redis tidak boleh menjadi source of truth untuk:
- status asset;
- subscription lifecycle;
- operation guard utama;
- version asset;
- ownership asset.
Cache key contoh:
asset:account:{accountId}:active-products:v{assetProjectionVersion}
asset:product-instance:{productInstanceId}:v{version}
subscription:{subscriptionId}:v{version}
Versioned key mengurangi risiko stale overwrite.
20. API Shape Awal
API detail akan dibahas di blok API-first, tetapi mental shape-nya perlu terlihat.
20.1 Query Installed Base
GET /accounts/{accountId}/installed-base
Response ringkas:
{
"accountId": "...",
"snapshotTime": "2026-07-02T10:30:00Z",
"products": [
{
"productInstanceId": "...",
"name": "Business Fiber 1Gbps",
"status": "ACTIVE",
"version": 7,
"subscriptionId": "...",
"relationships": [
{
"type": "PARENT_OF",
"targetProductInstanceId": "..."
}
]
}
]
}
20.2 Start Asset Modification
Biasanya API ini bukan public API untuk UI langsung. Ia dipanggil oleh order/workflow service.
POST /internal/assets/product-instances/{productInstanceId}/modification:start
Request:
{
"orderId": "...",
"orderItemId": "...",
"expectedVersion": 7,
"operationType": "MODIFY",
"correlationId": "..."
}
21. Failure Modes
21.1 Asset Updated Without History
Symptom:
product_instance status changed but no product_instance_history row exists
Impact:
- audit broken;
- dispute hard to investigate;
- support cannot explain change;
- downstream reconciliation becomes unreliable.
Mitigation:
- mutation only through repository/application service;
- DB trigger as defense-in-depth if needed;
- automated consistency check;
- outbox and history inserted in same transaction.
21.2 Two Active Changes On Same Asset
Symptom:
two in-progress orders targeting same product_instance
Impact:
- race condition;
- wrong fulfillment;
- stale quote conversion;
- inconsistent billing trigger.
Mitigation:
- asset operation guard;
- optimistic lock;
- stale snapshot validation;
- conflict policy.
21.3 Asset Status Too Technical
Symptom:
product_instance.status = WAITING_FOR_TECHNICIAN_CALL_BACK
Impact:
- product status becomes fulfillment workflow dump;
- UI/support confused;
- billing cannot interpret status;
- state transitions explode.
Mitigation:
- keep asset lifecycle high-level;
- put operational details in fulfillment task/workflow state;
- expose timeline combining asset + workflow events.
21.4 Subscription Treated As Product Instance
Symptom:
renewal creates duplicate product instances even when product did not change
Impact:
- installed base duplicated;
- billing duplicate risk;
- support sees multiple active products;
- amendment becomes hard.
Mitigation:
- model subscription explicitly;
- separate renewal from product modification;
- link product instances to subscription.
21.5 External System Changes Asset Without OMS Knowing
Symptom:
provisioning system disconnected service but OMS product_instance is ACTIVE
Impact:
- customer sees active but service down;
- billing continues incorrectly;
- next order uses wrong base state.
Mitigation:
- reconciliation job;
- external event ingestion;
- exception queue;
- manual repair command with audit.
22. Testing Strategy
22.1 Unit Tests For State Transition
Test examples:
ACTIVE can start MODIFY
ACTIVE can start DISCONNECT
PENDING_MODIFICATION cannot start DISCONNECT by default
DISCONNECTED cannot start MODIFY
SUSPENDED can resume
SUSPENDED can disconnect
22.2 Repository Tests
Use PostgreSQL test container or dedicated test database.
Test:
- insert product instance;
- update with correct version;
- update with stale version returns zero rows;
- history row inserted;
- active operation guard unique constraint works;
- relationship uniqueness works.
22.3 Command Handler Tests
Test:
- start modification acquires guard;
- start modification writes outbox;
- complete modification updates version;
- failed optimistic lock does not write event;
- business rule violation does not mutate DB.
22.4 Scenario Tests
Scenarios:
new sale -> product active -> subscription active
upgrade active product -> same product instance new version
disconnect product with child add-ons -> child impact evaluated
renew subscription -> subscription version changes, product unchanged
move service -> commercial product continuity preserved
23. Design Checklist
Sebelum melanjutkan ke API-first, asset model harus menjawab pertanyaan ini:
Can we know what the customer currently owns?
Can we know what the customer owned at quote time?
Can we know which order created or changed an asset?
Can we prevent two conflicting active changes?
Can we explain why a price or configuration was allowed?
Can we distinguish product, service, resource, and subscription?
Can we reconstruct history for audit and support?
Can we survive external system mismatch?
Can we safely modify/disconnect/move/renew?
Jika jawabannya belum jelas, order management berikutnya akan rapuh.
24. Ringkasan
Installed base adalah fondasi perubahan jangka panjang.
Tanpa asset dan subscription model yang benar, CPQ hanya bisa menjual produk baru. Ia akan kesulitan menangani modify, disconnect, move, renewal, amendment, dan reconciliation.
Prinsip utama part ini:
- catalog bukan installed base;
- order bukan asset;
- product instance berbeda dari service instance;
- service instance berbeda dari resource instance;
- subscription berbeda dari product instance;
- asset harus versioned;
- quote harus memakai installed base snapshot;
- order action harus eksplisit terhadap target asset;
- conflicting active changes harus dicegah;
- mutation harus melalui command yang audit-friendly;
- history dan outbox harus ditulis atomically;
- Redis bukan source of truth;
- Camunda tidak boleh langsung mengubah asset table.
Part berikutnya akan menutup domain modelling block dengan domain invariants and state machines. Di sana kita akan memaku aturan yang tidak boleh dilanggar oleh API, workflow, event consumer, repair script, atau operator.
You just completed lesson 11 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.