BUMN CRM Backend Services
Enterprise CRM microservices platform for a major state-owned enterprise (BUMN), migrating from a legacy billing system with an approval-workflow-driven architecture.
Overview
Backend services for a large state-owned enterprise (BUMN), built as part of PT. Noxus Ideata Prima. The system is an enterprise Customer Relationship Management (CRM) platform, built to replace a legacy billing system with a modern, modular architecture — covering account management, user management, and product & promo management.
flowchart TB
subgraph Client["Client"]
FE["React SPA<br/>single shell for all business modules"]
end
subgraph Gateway["Reverse Proxy (Nginx)"]
NG["Route by path prefix<br/>per business domain"]
end
subgraph Services["Business Microservices (Spring Boot)"]
ACC["Account Management Service"]
UM["User Management Service"]
PP["Product & Promo Service"]
NOTIF["Notification Service"]
end
subgraph Shared["Shared Libraries (Maven)"]
COMMON["Common Library<br/>security, JWT/LDAP, approval engine, notification client"]
CRM["Common CRM Library<br/>JPA entities + repositories for the whole schema"]
BASE["Common Base Library<br/>base entity, generic filter/spec framework"]
end
subgraph Infra["Infrastructure"]
ORA[("Oracle DB")]
KAFKA{{"Kafka<br/>notification topics"}}
MINIO[("Object Storage<br/>attachments")]
LDAP[("LDAP / Active Directory")]
end
FE --> NG
NG --> ACC & UM & PP & NOTIF
ACC --> COMMON & CRM
UM --> COMMON & CRM
PP --> COMMON & CRM
CRM --> BASE
ACC & UM & PP & CRM --> ORA
ACC & UM & PP -->|publish event| KAFKA --> NOTIF
NOTIF -->|real-time push| FE
ACC & UM & PP --> MINIO
UM --> LDAP
Each business service is deployed and scaled independently (its own Helm chart on Kubernetes), yet all three share a single Oracle schema through a common JPA library instead of defining their own entities — keeping the data model and most security/approval logic in one place rather than duplicated per service.
Problem
- The legacy billing system was rigid and hard to extend as the business grew.
- Sensitive data (accounts, pricing, promos, contracts) needed a traceable, multi-level approval process before any change could go live.
- Years of legacy billing data had to move into the new system without disrupting day-to-day operations.
Solution
- Modular services, each responsible for one business area (accounts, users, product & promo), so teams can build and release independently.
- Built-in approval workflow: every sensitive change is routed through the right approver before it takes effect, with a full audit trail.
- Real-time notifications so approvers and requesters are informed the moment a decision is needed or made.
- Staged data migration that moved legacy records into the new system safely, flagging anything that needed manual review instead of blocking the process.
Approval Workflow (shared across every module)
sequenceDiagram
participant U as User
participant SVC as Business Service<br/>(Account/Product/User)
participant APR as Approval Engine<br/>(Common Library)
participant DB as Oracle DB
participant KAFKA as Kafka
participant NOTIF as Notification Service
participant APV as Approver
U->>SVC: Submit change
SVC->>DB: Save/update business record (pending)
SVC->>APR: Register approval request
APR->>DB: Insert approval record (WAITING_FOR_APPROVAL)
APR->>KAFKA: publish notification.create
KAFKA->>NOTIF: consume event
NOTIF-->>APV: real-time notification
APV->>SVC: Open record detail
SVC->>APR: Check if current user is the approver
APR-->>SVC: true / false
SVC-->>APV: Show approval detail (only if authorized approver)
APV->>SVC: Approve / Reject
SVC->>DB: Update approval history + record status
SVC->>KAFKA: publish notification.broadcast (decision)
KAFKA->>NOTIF: consume
NOTIF-->>U: notify requester of decision
The same engine handles approvals for every domain — the current-level approver is resolved by matching the position required by the approval hierarchy against the requesting/approving user's own position, so a user isn't shown the approval action unless they're the authorized approver at that level.
Simplified Data Model
erDiagram
CUSTOMER ||--o{ ACCOUNT : "owns"
ACCOUNT ||--o{ SERVICE_AGREEMENT : "has"
ACCOUNT ||--o{ PROMO : "receives"
PRODUCT ||--o{ PRODUCT_VERSION : "versioned (copy-on-write)"
PRODUCT_VERSION ||--o{ PRICING : "priced"
PRODUCT_VERSION ||--o{ TERMS_OF_SERVICE : "governed by"
EMPLOYEE ||--o{ POSITION_ASSIGNMENT : "assigned"
POSITION_ASSIGNMENT }o--|| POSITION : "holds"
POSITION ||--o{ APPROVAL_HIERARCHY : "approval level"
APPROVAL ||--o{ APPROVAL_HISTORY : "decision trail"
ACCOUNT ||--o{ APPROVAL : "requires approval (via category)"
PRODUCT_VERSION ||--o{ APPROVAL : "requires approval (via category)"
PROMO ||--o{ APPROVAL : "requires approval (via category)"
The links into APPROVAL are conceptual rather than literal foreign keys — the approval table stores a generic record ID plus a category column identifying its originating domain, shared by every business module. Products follow a copy-on-write versioning pattern: changes create a new product-version row instead of mutating the existing one.
Key Contributions
- Built core API endpoints for account, service agreement, and pricing/promo management.
- Resolved data-sync issues between services sharing the same underlying data.
- Contributed to the legacy-to-new data migration effort.
- Delivered within enterprise-grade security and code-quality standards required for a BUMN client.