Bounded Contexts & Domain Model¶
Validated against PRD v1.0
Principle: Each bounded context owns its data. Contexts communicate through well-defined contracts, never through shared tables or internal imports.
1. Context Map¶
flowchart TB
CE[Config Engine<br/>templates, thresholds, rules]
WF[Workflow Orchestrator<br/>Onboarding Process]
CM[Case Management<br/>Onboarding Cases<br/>Screening Cases]
NS[Name Screening]
RR[Risk Rating]
NA[Network Analysis]
NT[Notification Engine]
CE -->|reads config| WF
WF -->|creates| CM
WF -->|invokes| NS
WF -->|invokes| RR
WF -->|invokes| NA
WF -->|triggers| NT
subgraph Decision [Decision Modules]
NS
RR
NA
NT
end
subgraph CrossCutting [Cross-Cutting]
AUDIT[Audit Service<br/>ALL contexts write]
IAM[Identity & Access RBAC<br/>ALL contexts check]
end
WF -.-> AUDIT
CM -.-> AUDIT
NS -.-> AUDIT
RR -.-> AUDIT
NA -.-> AUDIT
Context Relationships¶
| Upstream | Downstream | Relationship | Pattern |
|---|---|---|---|
| Configuration Engine | Workflow Orchestrator | Conformist — Workflow reads config, doesn't change it | Synchronous API call at workflow start |
| Configuration Engine | All Decision Modules | Conformist — Modules read thresholds/rules | Synchronous API call at module invocation |
| Workflow Orchestrator | Name Screening | Customer/Supplier — Workflow invokes, Screening responds | Async with callback |
| Workflow Orchestrator | Risk Rating | Customer/Supplier | Async with callback |
| Workflow Orchestrator | Network Analysis | Customer/Supplier | Async with callback |
| Workflow Orchestrator | Case Management | Partnership — close collaboration | Sync + events |
| Workflow Orchestrator | Notification | Customer/Supplier — fire and forget | Events |
| All Contexts | Audit Service | Conformist — all write, none read | Events / append-only log |
2. Bounded Context Details¶
2.1 Configuration Engine Context¶
Responsibility: Manage workflow templates, thresholds, rules, approval matrices, and document requirements. All config is versioned and immutable.
| Concept | Type | Description |
|---|---|---|
| WorkflowTemplate | Aggregate Root | Defines states, transitions, conditions, tasks, SLA timers for a workflow type |
| ThresholdConfig | Entity | Risk score boundaries (LOW/MEDIUM/HIGH) |
| ApprovalMatrix | Entity | Who can approve what type of decision |
| DocumentRequirementRule | Entity | Which documents are mandatory/optional per customer type + jurisdiction |
| EscalationRule | Entity | Routing rules for escalation (who gets what at which level) |
| ConfigVersion | Entity | Immutable snapshot of all config at a point in time |
| VersionNumber | Value Object | Semantic version of config |
| EffectiveDate | Value Object | When this config version became active |
Domain Events:
ConfigVersionCreatedConfigVersionPromoted(DRAFT → TEST → PROD)ConfigVersionRolledBack
2.2 Workflow Orchestration Context¶
Responsibility: Drive process state machines. Invoke decision modules. Manage human tasks. Enforce SLAs.
| Concept | Type | Description |
|---|---|---|
| WorkflowInstance | Aggregate Root | Running instance of a workflow. Tracks current state, config version, and execution context. |
| WorkflowState | Value Object | Current state in the state machine |
| WorkflowContext | Value Object | Customer ID, case ID, all data captured so far |
| Task | Entity | A work item — either system (invoke module) or human (review, approve) |
| TaskAssignment | Value Object | Who the task is assigned to |
| SLATimer | Entity | Deadline for a task with breach escalation rules |
| RetryPolicy | Value Object | Max retries, backoff strategy for module invocations |
| ModuleInvocation | Entity | Record of a decision module being called — request, response, timing |
Domain Events:
WorkflowStartedStateTransitionedTaskCreated/TaskAssigned/TaskCompletedSLAWarning/SLABreachedModuleInvoked/ModuleCompleted/ModuleFailed
2.3 Customer Onboarding Context¶
Responsibility: Manage the customer onboarding lifecycle — intake, classification, data capture, document management, identity validation, UBO capture, and final decision.
| Concept | Type | Description |
|---|---|---|
| Customer | Aggregate Root | The party being onboarded. Can be an individual or organization. |
| Individual | Entity | Natural person details (name, DOB, nationality, PEP flags) |
| LegalEntity | Entity | Registered organization details (name, registration number, incorporation country) |
| Address | Value Object | Physical or registered address |
| OwnershipRelationship | Entity | Parent → child ownership with percentage and control type |
| UBO | Entity | Ultimate Beneficial Owner — derived from ownership chain traversal |
| Document | Entity | Uploaded evidence (passport, incorporation cert, utility bill) |
| DocumentValidation | Value Object | Validation status (PENDING, VERIFIED, EXPIRED, REJECTED) |
| ApplicationIntake | Entity | The initial submission from the RM |
| Classification | Value Object | Customer archetype (Retail, SME, Corporate, etc.) |
| OnboardingDecision | Entity | Final decision: APPROVED, APPROVED_WITH_RESTRICTIONS, REJECTED, etc. |
Domain Events:
CustomerCreatedDocumentUploaded/DocumentValidatedIdentityVerified/IdentityMismatchUBOIdentifiedOnboardingDecisionMade
2.4 Name Screening Context¶
Responsibility: Match customer names against watchlists (sanctions, PEP, internal). Return match results and support analyst adjudication.
| Concept | Type | Description |
|---|---|---|
| ScreeningRequest | Aggregate Root | A screening invocation for a specific subject |
| ScreeningResult | Entity | Match outcome: NO_MATCH, POTENTIAL_MATCH, CONFIRMED_MATCH |
| MatchDetail | Value Object | Matched list entry, similarity score, matched fields |
| Watchlist | Entity | External list metadata (source, version, last updated) |
| Adjudication | Entity | Analyst decision: CLEAR, CONFIRM, REQUEST_INFO |
| ScreeningSubject | Value Object | Name, aliases, DOB, nationality, identifiers — the entity being screened |
| MatchAlgorithm | Value Object | EXACT or FUZZY, with configurable threshold |
Domain Events:
ScreeningRequestedMatchFound/NoMatchFoundAdjudicationMade
2.5 Customer Risk Rating Context¶
Responsibility: Calculate risk scores using configurable rules. Produce explainable risk bands.
| Concept | Type | Description |
|---|---|---|
| RiskAssessment | Aggregate Root | A risk evaluation for a customer |
| RiskFactor | Entity | A single contributing factor (geography, customer type, PEP, product, etc.) |
| RiskScore | Value Object | Numeric score |
| RiskBand | Value Object | LOW, MEDIUM, or HIGH |
| RiskMethodology | Value Object | Config version of the rules used |
| FactorWeight | Value Object | Weight assigned to a factor |
Domain Events:
RiskAssessmentRequestedRiskAssessmentCompleted
2.6 Case Management Context¶
Responsibility: Manage investigation cases — lifecycle, assignment, evidence, notes, escalation, decisions.
| Concept | Type | Description |
|---|---|---|
| Case | Aggregate Root | An investigation container. Links tasks, evidence, decisions. |
| CaseType | Value Object | ONBOARDING_REVIEW, SANCTIONS_INVESTIGATION, EDD_REVIEW, etc. |
| CaseState | Value Object | CREATED → ASSIGNED → IN_PROGRESS → PENDING_REVIEW → ESCALATED → DECIDED → CLOSED |
| CaseNote | Entity | Append-only note with author and timestamp |
| Evidence | Entity | Document or data attached to the case |
| Decision | Entity | A formal decision with rationale, actor, and timestamp |
| CaseAssignment | Value Object | Who is currently assigned |
Domain Events:
CaseCreated/CaseAssigned/CaseEscalatedNoteAddedEvidenceAttachedDecisionMadeCaseClosed
2.7 Network Analysis Context¶
Responsibility: Map ownership structures and discover linked entities.
| Concept | Type | Description |
|---|---|---|
| EntityGraph | Aggregate Root | The graph of entities related to a customer |
| GraphNode | Value Object | An entity in the graph (customer, UBO, intermediate company) |
| GraphEdge | Value Object | A relationship with type (OWNS, DIRECTS, SHARES_ADDRESS) |
| LinkedEntity | Entity | An entity discovered through shared attributes |
| OwnershipChain | Entity | A traversal path from customer to UBO |
| ControlType | Value Object | DIRECT, INDIRECT, BENEFICIAL |
Domain Events:
GraphBuiltLinkedEntityDiscovered
2.8 Notification Context¶
Responsibility: Send in-platform and external notifications.
| Concept | Type | Description |
|---|---|---|
| Notification | Aggregate Root | A message to a user |
| NotificationType | Value Object | TASK_ASSIGNED, SLA_WARNING, ESCALATION, STATUS_CHANGE |
| DeliveryChannel | Value Object | IN_PLATFORM, EMAIL (P1) |
| NotificationStatus | Value Object | UNREAD, READ, ACKNOWLEDGED |
Domain Events:
NotificationSentNotificationAcknowledged
2.9 Audit & Governance Context¶
Responsibility: Record every system action immutably. Support deterministic replay.
| Concept | Type | Description |
|---|---|---|
| AuditEvent | Aggregate Root | Immutable record of one action |
| EventType | Value Object | STATE_TRANSITION, MODULE_INVOCATION, DECISION, CONFIG_CHANGE, DATA_ACCESS |
| Actor | Value Object | Who performed the action (user ID or SYSTEM) |
| CorrelationId | Value Object | Links events across contexts for one request |
| AuditTrail | Entity | The complete ordered sequence of events for a case or customer |
| ReplayRequest | Entity | An auditor's request to reconstruct a decision |
Domain Events:
AuditEventRecorded(this is the fundamental event — everything else becomes an audit event)
2.10 Identity & Access Context¶
Responsibility: Authentication, authorization, role management.
| Concept | Type | Description |
|---|---|---|
| User | Aggregate Root | A platform user |
| Role | Entity | RM, KYC_ANALYST, SANCTIONS_ANALYST, EDD_ANALYST, FCC_REVIEWER, SUPERVISOR, AUDITOR, ADMIN |
| Permission | Value Object | What a role can do (CREATE_CASE, APPROVE_DECISION, VIEW_CUSTOMER, etc.) |
| Session | Entity | Active user session with expiry |
Domain Events:
UserAuthenticated/AuthenticationFailedRoleAssignedAccessDenied
3. Canonical Domain Model¶
Shared entities across all contexts:
Customer (Aggregate Root)
├── customerId: UUID
├── customerType: INDIVIDUAL | LEGAL_ENTITY
├── individual: Individual?
├── legalEntity: LegalEntity?
├── addresses: List<Address>
├── riskStatus: RiskBand?
├── lifecycleStatus: ACTIVE | SUSPENDED | CLOSED
├── createdAt: Instant
└── updatedAt: Instant
Individual (Entity)
├── individualId: UUID
├── firstName: String
├── lastName: String
├── aliases: List<String>
├── dateOfBirth: LocalDate
├── nationality: Country
├── residenceCountry: Country
├── taxResidency: Country?
├── occupation: String?
├── pepFlag: Boolean
├── pepLevel: PEP_LEVEL?
├── sourceOfWealth: String?
└── sourceOfFunds: String?
LegalEntity (Entity)
├── entityId: UUID
├── legalName: String
├── tradeName: String?
├── registrationNumber: String
├── incorporationCountry: Country
├── legalForm: LEGAL_FORM
├── industryCode: String?
└── incorporationDate: LocalDate?
OwnershipRelationship (Entity)
├── relationshipId: UUID
├── parentEntityId: UUID
├── childEntityId: UUID
├── ownershipPercentage: BigDecimal
├── controlType: DIRECT | INDIRECT | BENEFICIAL
├── effectiveFrom: LocalDate
├── effectiveTo: LocalDate?
└── confidenceScore: Double
Document (Entity)
├── documentId: UUID
├── documentType: PASSPORT | INCORPORATION_CERT | SHAREHOLDER_REGISTER | DIRECTOR_ID | UBO_DECLARATION | PROOF_ADDRESS | SOURCE_WEALTH
├── issueDate: LocalDate?
├── expiryDate: LocalDate?
├── validationStatus: PENDING | VERIFIED | EXPIRED | REJECTED
├── storageReference: String
└── uploadedAt: Instant
ScreeningResult (Entity)
├── resultId: UUID
├── screeningType: SANCTIONS | PEP | INTERNAL
├── matchStatus: NO_MATCH | POTENTIAL_MATCH | CONFIRMED_MATCH
├── matchedEntry: String?
├── matchScore: Double?
├── matchFields: List<String>
├── adjudication: CLEAR | CONFIRM | REQUEST_INFO?
├── adjudicationRationale: String?
└── createdAt: Instant
RiskAssessment (Entity)
├── assessmentId: UUID
├── methodologyVersion: String
├── riskScore: Int
├── riskBand: LOW | MEDIUM | HIGH
├── factors: List<RiskFactor>
├── createdAt: Instant
└── validUntil: Instant?
RiskFactor (Value Object)
├── factorName: String
├── factorScore: Int
└── rationale: String
Case (Aggregate Root)
├── caseId: UUID
├── caseType: ONBOARDING_REVIEW | SANCTIONS_INVESTIGATION | EDD_REVIEW | QA_REVIEW
├── state: CaseState
├── priority: LOW | MEDIUM | HIGH | CRITICAL
├── customerId: UUID
├── workflowInstanceId: UUID?
├── assignedTo: UUID?
├── escalationLevel: Int
├── createdAt: Instant
├── slaDeadline: Instant?
├── resolvedAt: Instant?
└── resolution: DECISION?
Decision (Entity)
├── decisionId: UUID
├── decisionType: APPROVED | APPROVED_WITH_RESTRICTIONS | REJECTED | ESCALATED | CLEARED | CONFIRMED | PROHIBITED
├── actorType: USER | SYSTEM
├── actorId: UUID?
├── rationale: String
├── evidenceRefs: List<UUID>
├── configVersion: String
├── overrideJustification: String?
└── timestamp: Instant
WorkflowInstance (Aggregate Root)
├── workflowInstanceId: UUID
├── workflowDefinitionId: String
├── definitionVersion: String
├── currentState: String
├── context: JsonNode (customerId, caseId, captured data)
├── startedAt: Instant
├── completedAt: Instant?
└── status: RUNNING | COMPLETED | FAILED | CANCELLED
AuditEvent (Aggregate Root)
├── eventId: Long (sequential)
├── correlationId: UUID
├── eventType: STATE_TRANSITION | MODULE_INVOCATION | DECISION | CONFIG_CHANGE | DATA_ACCESS | AUTH
├── actorType: USER | SYSTEM
├── actorId: UUID?
├── contextType: String (CUSTOMER | CASE | WORKFLOW)
├── contextId: UUID?
├── action: String
├── payload: JsonNode
├── configVersion: String?
└── timestamp: Instant
4. Integration Patterns¶
4.1 Synchronous (within modular monolith)¶
Used when the caller needs the result to continue:
- Workflow → Screen customer (invoke, wait, get result)
- Workflow → Rate risk (invoke, wait, get result)
- All modules → Fetch config (get active version)
Implementation: Direct method calls across module boundaries, through interface contracts. In Spring Boot: @Service interface in a shared API module, implementation in the domain module.
4.2 Asynchronous (events)¶
Used when the caller does not need the result:
- Any module → Audit Service (fire and forget)
- State transition → Notification (fire and forget)
- Case state change → Workflow (trigger next state)
Implementation: Spring Application Events within the monolith. If extracted to services: Kafka or RabbitMQ.
4.3 Module Contract (Decision Module Standard)¶
Every decision module (Screening, Risk Rating, Network Analysis, Entity Resolution) must implement:
interface DecisionModule<I, O> {
fun execute(request: ModuleRequest<I>): ModuleResponse<O>
}
data class ModuleRequest<I>(
val requestId: UUID,
val moduleType: String,
val entityReference: UUID,
val payload: I,
val context: Map<String, Any>,
val configVersion: String
)
data class ModuleResponse<O>(
val requestId: UUID,
val status: ModuleStatus,
val decision: O?,
val score: Double?,
val rationale: String?,
val evidence: List<EvidenceRef>?,
val executionMetadata: ExecutionMetadata
)
enum class ModuleStatus { SUCCESS, FAILURE, TIMEOUT, EXTERNAL_UNAVAILABLE }
This is the contract that enables pluggable modules. Any new intelligence capability (Adverse Media, Transaction Monitoring in v2) just implements this interface.
5. Module Boundaries (What NOT to Do)¶
| Violation | Why It's Bad |
|---|---|
| Onboarding context imports from Screening.internal package | Breaks modularity → extraction becomes impossible |
| Case Management reads Workflow DB tables directly | Data ownership violation → stale reads, coupling |
| Risk Rating calls Screening to get data | Should go through Workflow (orchestrator), not peer-to-peer |
| Audit Service queries Case Management for context | Audit is write-only. Context is pushed to it, not pulled. |
| Shared "common" module with everything in it | Becomes a dumping ground. Shared kernel limited to: IDs, value objects (Address, Money), module contract interfaces. |
6. Architecture Fitness Tests¶
Tests that run in CI to prevent boundary violations:
class ArchitectureFitnessTest {
@Test
fun `onboarding module must not import from screening internals`() {
noClasses()
.that().resideInAPackage("..onboarding..")
.should().dependOnClassesThat()
.resideInAPackage("..screening.internal..")
.check(importedClasses)
}
@Test
fun `audit module must not depend on any domain module`() {
noClasses()
.that().resideInAPackage("..audit..")
.should().dependOnClassesThat()
.resideInAPackage("..onboarding..")
.orShould().dependOnClassesThat()
.resideInAPackage("..screening..")
// ... etc for all domain modules
.check(importedClasses)
}
@Test
fun `no circular dependencies between modules`() {
slices()
.matching("com.fec.platform.(*)..")
.should().beFreeOfCycles()
}
}
Domain model derived from PRD v1.0 requirements. Re-evaluate if new bounded contexts are added or existing ones split.