System Topology & Integration Patterns¶
Validated against PRD v1.0
1. Modular Monolith Topology¶
1.1 Package Structure¶
flowchart TB
subgraph API[api/ — REST Controllers]
ONB_API[onboarding]
SCR_API[screening]
RR_API[riskrating]
CM_API[casemanagement]
NA_API[networkanalysis]
CFG_API[configuration]
AUD_API[audit]
AUTH_API[auth]
end
subgraph DOM[domain/ — Bounded Contexts]
ONB[onboarding]
SCR[screening]
RR[riskrating]
CM[casemanagement]
NA[networkanalysis]
CFG[configuration]
NT[notification]
AUD[audit]
AUTH[auth]
end
subgraph SHARED[shared/ — Shared Kernel]
MODEL[model]
CONTRACT[contract]
EVENT[event]
end
subgraph INFRA[infra/ — Infrastructure]
PERSIST[persistence]
MSG[messaging]
TEMP[temporal]
SEC[security]
EXT[external]
end
API --> DOM
DOM --> SHARED
INFRA --> DOM
1.2 Module Dependency Rules¶
Allowed:
api.X → domain.X (controller calls its own domain service)
domain.X → shared.contract (implements shared interface)
domain.X → shared.model (uses shared value objects)
infra → domain.* (infrastructure implements domain interfaces)
Forbidden:
domain.X → domain.Y.internal (cross-module internal imports)
domain.X → api.Y (domain must not depend on API layer)
domain.X → infra (domain must not depend on infrastructure)
2. Communication Patterns¶
2.1 Synchronous (In-Process)¶
When: Caller needs the result to proceed.
How: Direct method call through interface contract in shared.contract.
Example:
// shared/contract/ScreeningModule.kt
interface ScreeningModule {
fun screen(request: ScreeningRequest): ScreeningResponse
}
// domain/screening/ScreeningServiceImpl.kt (implements)
// domain/onboarding/OnboardingService.kt (calls via interface)
// Wiring: dependency injection resolves ScreeningModule → ScreeningServiceImpl
Used for:
- Workflow invokes Screening
- Workflow invokes Risk Rating
- Workflow invokes Network Analysis
- All modules fetch config from Configuration Engine
- All modules write to Audit Service
2.2 Asynchronous (In-Process Events)¶
When: Caller does not need the result. Fire and forget.
How: Spring Application Events — publish in one module, handle in another.
Example:
// domain/casemanagement — publishes
eventPublisher.publishEvent(CaseEscalated(caseId, escalationLevel, timestamp))
// domain/notification — handles
@EventListener
fun onCaseEscalated(event: CaseEscalated) {
notificationService.send(SLA_WARNING, event.caseId)
}
// domain/audit — handles
@EventListener
fun onCaseEscalated(event: CaseEscalated) {
auditWriter.record(AuditEvent(event))
}
Used for:
- State transitions → Audit Service
- SLA breaches → Notification
- Case reassignment → Notification
- Decision made → Audit Service
2.3 Temporal Workflows (External Orchestration)¶
When: Long-running workflows with retries, timers, and human-in-the-loop.
How: Temporal SDK — workflow code calls activities, activities call domain services.
flowchart TB
TW[Temporal Worker]
TW --> WF[CustomerOnboardingWorkflow]
WF --> A1[screenCustomer<br/>→ ScreeningModule]
WF --> A2[rateRisk<br/>→ RiskRatingModule]
WF --> A3[analyzeNetwork<br/>→ NetworkAnalysisModule]
WF --> A4[waitForApproval<br/>→ Human Signal]
WF --> A5[makeDecision<br/>→ CaseManagement]
Temporal provides: retry, timeout, versioning, durability (survives process restart), visibility (Temporal UI).
3. API Gateway¶
MVP: Spring Boot Controllers (No Separate Gateway)¶
In the modular monolith, the API layer is co-located:
flowchart LR
B[Browser] -->|HTTPS| SB[Spring Boot :8443]
SB --> ONB[api/onboarding]
SB --> SCR[api/screening]
SB --> CM[api/casemanagement]
SB --> CFG[api/configuration]
SB --> AUD[api/audit]
Why no separate API Gateway in MVP:
- Single deployment. No need for request routing between services.
- Spring Boot handles TLS, authentication (Spring Security Filter), CORS, rate limiting (Bucket4j).
- If extracted to services later, add a gateway (Kong, Spring Cloud Gateway) then.
API Conventions¶
All endpoints follow the standard defined in NFR-A01:
GET /api/v1/cases?page=1&limit=50
POST /api/v1/cases { "caseType": "ONBOARDING_REVIEW", ... }
GET /api/v1/cases/{caseId}
PATCH /api/v1/cases/{caseId} { "assignedTo": "user-456" }
POST /api/v1/cases/{caseId}/decisions { "decisionType": "APPROVED", ... }
Headers:
Authorization: Bearer <jwt>Idempotency-Key: <uuid>(for POST/PUT/PATCH)X-Correlation-Id: <uuid>(generated if not provided)
4. Deployment Topology¶
MVP: Single Deployment¶
flowchart TB
subgraph DC[Docker Compose]
APP[Spring Boot App<br/>Temporal Worker<br/>React SPA<br/>Spring Security<br/>All 10 contexts]
DB[(PostgreSQL)]
TS[Temporal Server]
APP --> DB
APP --> TS
end
Ports:
- App: 8443 (HTTPS)
- PostgreSQL: 5432 (internal only)
- Temporal UI: 8080 (dev only, blocked in production)
v2: Multi-Service (When Extracted)¶
flowchart TB
GW[Gateway<br/>Kong / SCG]
ONB[Onboarding Service]
SCR[Screening Service]
GW --> ONB
GW --> SCR
ONB --> KB[Event Bus<br/>Kafka]
SCR --> KB
5. Cross-Cutting Concerns¶
5.1 Audit Logging¶
Every bounded context calls AuditWriter.record(event). Implementation: writes to PostgreSQL immutable audit table. Post-MVP: also publishes to Kafka for external consumers.
Where it's called:
@AroundAOP advice on allapi/*controller methods- Explicitly in domain services for state transitions
- Temporal activities wrap results with audit hooks
5.2 Correlation ID Propagation¶
Generated at API gateway (Spring Filter). Stored in MDC (Mapped Diagnostic Context). Passed to Temporal workflows as workflow metadata. All logs and audit events include it.
5.3 Configuration Resolution¶
Every module calls Configuration Engine at invocation time to get the active config version:
val config = configEngine.getActiveConfig(moduleType, entityType, jurisdiction)
// config.version is stored with every module output for audit traceability
5.4 Error Handling¶
Global exception handler (@ControllerAdvice) returns consistent error format:
{
"error": {
"code": "CASE_NOT_FOUND",
"message": "No case found with ID 123e4567",
"refId": "abc-123"
}
}
Domain exceptions propagate through Temporal activities → workflow decides: retry, escalate, or fail.
6. Extraction Path: Monolith → Services¶
When to Extract a Module¶
| Signal | Action |
|---|---|
| Module has different scaling needs (e.g., screening called 100x more than config) | Extract to separate deployable |
| Module has different deployment cadence (e.g., audit changes rarely, onboarding changes weekly) | Extract for independent deployment |
| Module needs different data storage (e.g., graph DB for Network Analysis) | Extract with own database |
| Team grows and owns specific modules | Extract to enable team autonomy |
| Performance isolation needed (e.g., screening latency affecting UI) | Extract to isolate resources |
Extraction Steps (When Ready)¶
- Module already has a clean interface in
shared.contract— no code changes needed in callers - Create a new Spring Boot app with the module's domain + infra packages
- Replace in-process interface implementation with HTTP client that calls the new service
- Replace Spring Events with Kafka topics for cross-service events
- Run both old and new with feature flags, then cut over
This is why the modular monolith matters. If boundaries are clean at the module level, extraction is mechanical — not a rewrite.
7. Integration with External Services¶
Pattern: Adapter + Fallback¶
Every external integration has:
1. Interface in shared.contract (e.g., SanctionsListProvider)
2. Real adapter in infra.external (calls the external API)
3. Mock adapter for tests (returns canned data)
4. Fallback behavior: if external service unavailable, mark task as BLOCKED, notify operator, retry when available (NFR-R02)
interface SanctionsListProvider {
fun screen(request: SanctionsScreenRequest): SanctionsScreenResponse
}
class RestSanctionsProvider(private val client: WebClient) : SanctionsListProvider { ... }
class MockSanctionsProvider : SanctionsListProvider { ... } // for dev/test
class FallbackSanctionsProvider(private val delegate: SanctionsListProvider) : SanctionsListProvider {
override fun screen(request: SanctionsScreenRequest): SanctionsScreenResponse {
return try {
delegate.screen(request)
} catch (e: Exception) {
SanctionsScreenResponse(status = EXTERNAL_UNAVAILABLE, ...)
}
}
}
8. Security Topology¶
flowchart LR
B[Browser] -->|TLS| JWT[JWT Auth]
JWT --> RBAC[RBAC Filter]
RBAC --> RATE[Rate Limit]
RATE --> CORR[Correlation ID]
CORR --> CTRL[Controller]
SA[Service A] -->|mTLS| SB[Service B]
SB --> VAL[Validate JWT + RBAC]
Topology designed for PRD v1.0 MVP (single deployment, modular monolith). Re-evaluate at v2 when service extraction begins.