Domain Specification: Name Screening¶
Validated against PRD v1.0 — All FR-NS-* requirements implemented here. See Traceability Matrix.
1. Context Overview¶
Bounded Context: screening
Responsibility: Match customer identities against watchlists (sanctions, PEP, internal). Return match results with confidence scores. Support analyst adjudication.
Owns: ScreeningRequest, ScreeningResult, Adjudication aggregates.
Depends On: Configuration Engine (matching thresholds), Case Management (adjudication tasks), Audit Service (logging).
Events Published: ScreeningRequested, MatchFound, NoMatchFound, AdjudicationMade.
2. Matching Algorithms¶
2.1 Algorithm Chain¶
flowchart TD
A[Input: ScreeningSubject] --> B[1. Name Normalization<br/>NFKD + case fold + whitespace<br/>+ title removal + diacritic removal]
B --> C{2. Exact Match?}
C -->|Yes| D[CONFIRMED_MATCH<br/>score: 1.0]
C -->|No| E[3. Fuzzy Match<br/>Levenshtein + Metaphone<br/>+ Token sort ratio]
E --> F{Fuzzy ≥ threshold?}
F -->|Yes| G[POTENTIAL_MATCH<br/>score: 0.6-0.95]
F -->|No| H[4. Alias Match<br/>Exact + fuzzy on aliases]
H --> I{Alias match?}
I -->|Yes| J[POTENTIAL_MATCH<br/>with alias flag]
I -->|No| K[5. Identity Corroboration<br/>DOB + nationality + identifiers]
G --> K
J --> K
K --> L[Result: matchStatus<br/>matchScore, matchDetails]
2.2 Matching Thresholds (Configurable)¶
{
"exactMatch": { "enabled": true },
"fuzzy": {
"enabled": true,
"maxEditDistance": 2,
"minTokenSortRatio": 0.85,
"phoneticMatchRequired": false
},
"aliasMatch": { "enabled": true },
"identityCorroboration": {
"dobWeight": 0.10,
"nationalityWeight": 0.05,
"identifierWeight": 0.15
},
"potentialMatchThreshold": 0.60,
"confirmedMatchThreshold": 0.95
}
3. Data Model¶
3.1 ScreeningRequest¶
data class ScreeningRequest(
val requestId: UUID,
val customerId: UUID,
val workflowInstanceId: UUID,
val subjects: List<ScreeningSubject>, // customer + all UBOs
val listTypes: List<ListType>, // SANCTIONS, PEP, INTERNAL
val configVersion: String,
val createdAt: Instant
)
data class ScreeningSubject(
val subjectRef: String, // "CUSTOMER", "UBO-{entityId}"
val fullName: String,
val aliases: List<String>,
val dateOfBirth: LocalDate?,
val nationality: Country?,
val identifiers: List<Identifier> // passport, national ID, etc.
)
enum class ListType { SANCTIONS, PEP, INTERNAL }
3.2 ScreeningResult¶
data class ScreeningResult(
val screeningId: UUID,
val requestId: UUID,
val subjectRef: String,
val matchStatus: MatchStatus, // NO_MATCH, POTENTIAL_MATCH, CONFIRMED_MATCH
val matchScore: Double, // 0.0 - 1.0
val matches: List<WatchlistMatch>,
val listVersions: Map<ListType, String>, // which list version was used
val executionTimeMs: Long,
val createdAt: Instant
)
enum class MatchStatus { NO_MATCH, POTENTIAL_MATCH, CONFIRMED_MATCH }
data class WatchlistMatch(
val matchId: UUID,
val listType: ListType,
val listName: String, // "OFAC SDN", "EU Consolidated", "PEP List v3"
val matchedEntryId: String,
val matchedName: String,
val matchType: MatchType, // EXACT, FUZZY, ALIAS
val matchedFields: List<MatchedField>,
val score: Double,
val entrySummary: String // "John SMITH, DOB 1975-03-15, Nationality: IR"
)
enum class MatchType { EXACT, FUZZY, ALIAS }
data class MatchedField(
val fieldName: String, // "fullName", "dateOfBirth", "nationality", "identifier"
val subjectValue: String,
val matchedValue: String,
val similarity: Double
)
3.3 Adjudication¶
data class Adjudication(
val adjudicationId: UUID,
val resultId: UUID,
val matchId: UUID?,
val decision: AdjudicationDecision, // CLEAR, CONFIRM, REQUEST_INFO
val actorId: UUID,
val rationale: String, // mandatory
val additionalEvidence: List<UUID>?, // documents supporting decision
val createdAt: Instant
)
enum class AdjudicationDecision { CLEAR, CONFIRM, REQUEST_INFO }
4. Watchlist Provider Contract¶
interface WatchlistProvider {
/** Returns the supported list types this provider handles */
fun supportedListTypes(): List<ListType>
/** Returns metadata about the current list version */
fun getListVersions(): Map<ListType, String>
/** Screen a batch of subjects against this provider's lists */
fun screen(request: BatchScreenRequest): BatchScreenResponse
}
data class BatchScreenRequest(
val requestId: UUID,
val subjects: List<ScreeningSubject>,
val listTypes: List<ListType>
)
data class BatchScreenResponse(
val requestId: UUID,
val results: List<SubjectMatchResult>,
val listVersions: Map<ListType, String>,
val providerId: String
)
data class SubjectMatchResult(
val subjectRef: String,
val matches: List<WatchlistMatch>
)
Implementations:
- RestWatchlistProvider — calls external REST API (OFAC, EU, UN, commercial providers)
- MockWatchlistProvider — returns canned data for development/testing
- CachingWatchlistProvider — wraps another provider with TTL cache (list data cached, screening still against cached data)
5. Screening Flow¶
flowchart TD
WF[Workflow invokes Screening<br/>via Temporal activity] --> CF[1. Fetch active config]
CF --> REQ[2. Build ScreeningRequest<br/>3. Normalize names]
REQ --> SCR[4. Screen against providers<br/>parallel: sanctions + PEP]
SCR --> RES{Match Status?}
RES -->|NO_MATCH| AUTO[Auto-close]
RES -->|POTENTIAL_MATCH| TASK[Create adjudication task<br/>→ analyst queue]
RES -->|CONFIRMED_MATCH| BLOCK[Auto-block workflow<br/>PROHIBITED]
6. API Contracts¶
6.1 Screen Customer (Internal — called by Workflow Engine)¶
POST /api/v1/screening/screen
Authorization: System
Request:
{
"customerId": "uuid",
"workflowInstanceId": "uuid",
"subjects": [
{
"subjectRef": "CUSTOMER",
"fullName": "ACME Holdings B.V.",
"aliases": ["ACME Holding", "ACME BV"],
"nationality": "NLD"
},
{
"subjectRef": "UBO-uuid-1",
"fullName": "John SMITH",
"dateOfBirth": "1975-03-15",
"nationality": "GBR",
"identifiers": [{"type": "PASSPORT", "value": "GB12345678"}]
}
],
"listTypes": ["SANCTIONS", "PEP"],
"configVersion": "1.0.3"
}
Response 200:
{
"requestId": "uuid",
"overallStatus": "POTENTIAL_MATCH",
"results": [
{
"subjectRef": "CUSTOMER",
"matchStatus": "NO_MATCH",
"matchScore": 0.0,
"matches": []
},
{
"subjectRef": "UBO-uuid-1",
"matchStatus": "POTENTIAL_MATCH",
"matchScore": 0.85,
"matches": [
{
"matchId": "uuid",
"listType": "PEP",
"listName": "PEP List v3",
"matchedEntryId": "PEP-2024-0042",
"matchedName": "John SMITH",
"matchType": "EXACT",
"matchedFields": [
{ "fieldName": "fullName", "subjectValue": "John SMITH", "matchedValue": "John SMITH", "similarity": 1.0 },
{ "fieldName": "dateOfBirth", "subjectValue": "1975-03-15", "matchedValue": "1975-03-15", "similarity": 1.0 },
{ "fieldName": "nationality", "subjectValue": "GBR", "matchedValue": "GBR", "similarity": 1.0 }
],
"score": 0.85,
"entrySummary": "John SMITH, DOB 1975-03-15, Nationality: GBR. PEP Level: NATIONAL. Position: Member of Parliament."
}
]
}
],
"listVersions": {
"SANCTIONS": "2025-05-20",
"PEP": "2025-05-18"
},
"executionTimeMs": 2340
}
6.2 Adjudicate Match¶
POST /api/v1/screening/results/{resultId}/matches/{matchId}/adjudicate
Authorization: Bearer <jwt>
Request:
{
"decision": "CLEAR",
"rationale": "Name and DOB match, but the PEP is a UK Member of Parliament, and our customer is a Dutch national residing in Netherlands with no political exposure. Different nationality and residence confirm false positive."
}
Response 201:
{
"adjudicationId": "uuid",
"decision": "CLEAR",
"screeningStatus": "NO_MATCH"
}
6.3 Get Pending Adjudications¶
GET /api/v1/screening/adjudications/pending?page=1&limit=20
Authorization: Bearer <jwt>
Response 200:
{
"items": [
{
"resultId": "uuid",
"customerId": "uuid",
"customerName": "ACME Holdings B.V.",
"subjectName": "John SMITH",
"matchStatus": "POTENTIAL_MATCH",
"matchCount": 1,
"createdAt": "2025-06-01T12:00:00Z",
"slaRemaining": "1h 45m"
}
],
"total": 3,
"page": 1
}
7. Error Handling & Edge Cases¶
| Scenario | Behavior |
|---|---|
| All watchlist providers unavailable | Return status: EXTERNAL_UNAVAILABLE. Workflow task marked BLOCKED. Retry with backoff (1s, 2s, 4s). |
| Single provider unavailable | Use results from available providers. Mark unavailable provider in response metadata. Log warning. |
| Subject has no name | 400 Bad Request. "At least one subject name is required per screening request." |
| Empty aliases | Aliases array defaults to empty. No error. |
| List type not supported by any provider | 400 Bad Request. "No provider registered for list type: {type}." |
| Adjudicate already-adjudicated match | 409 Conflict. "Match {matchId} was already adjudicated as {decision} by {actor} on {date}." |
| Adjudication without rationale | 400 Bad Request. "Rationale is required for all adjudication decisions." |
| Large batch ( > 50 subjects) | Accepted but processed asynchronously. Returns 202 Accepted with polling URL. |
| Name contains non-Latin script (e.g., Arabic, Cyrillic) | Transliteration fallback. If no transliteration available: flag as UNABLE_TO_SCREEN in result, creates manual task. |
| List update mid-screening | Version is captured at request time. Screening uses that version. Concurrent list update does not affect in-flight screening. |
8. Performance Targets¶
| Metric | Target |
|---|---|
| Single subject screening | ≤ 2 seconds (p95) |
| Batch of 10 subjects | ≤ 5 seconds (p95) |
| Adjudication decision | User action completes in ≤ 300ms |
| Watchlist data freshness | Lists refreshed every 24h (configurable per provider) |
Spec validated against PRD v1.0 requirements FR-NS-01 through FR-NS-03. Re-evaluate if matching algorithms or provider contracts change.