Skip to content

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.