Counterparties Domain¶
Master data for merchants, payment processors, banks, and other counterparties. Low velocity reference data with moderate retention requirements.
Overview¶
| Attribute | Value |
|---|---|
| Owner | Counterparties Domain Team |
| Contact | counterparties@chakra.fintech |
| Data Products | partner-master |
| Kafka Topics | counterparties-raw |
| Iceberg Namespace | counterparties |
| Freshness SLA | 60 minutes |
| Availability SLA | 99.9% |
| Retention | 2 years |
| Velocity | Very low (reference data) |
| Approval Required | YES (commercial data) |
| Status | Production |
Data Products¶
partner-master¶
Master data for merchants, banks, payment processors, and other counterparties.
Schema (Avro v1.0):
{
"type": "record",
"name": "Counterparty",
"fields": [
{"name": "counterparty_id", "type": "string"},
{"name": "counterparty_type", "type": {"type": "enum", "symbols": ["MERCHANT", "BANK", "PROCESSOR", "PARTNER"]}},
{"name": "counterparty_name", "type": "string"},
{"name": "merchant_category_code", "type": ["null", "string"]},
{"name": "credit_limit", "type": "double"},
{"name": "settlement_period", "type": "int"},
{"name": "fee_percentage", "type": "double"},
{"name": "status", "type": {"type": "enum", "symbols": ["ACTIVE", "INACTIVE", "SUSPENDED"]}}
]
}
Iceberg Table (counterparties.merchants): - Partitions: [year, month] - Format: Parquet - Snapshots: Every 60 minutes
SLAs:
freshness:
value: 60
unit: minutes
definition: "Merchant master data updates within 1 hour"
availability:
value: 99.9
unit: percent
completeness:
value: 100
unit: percent
Quality Rules:
- name: positive_credit_limit
rule: "credit_limit > 0"
impact: critical
- name: valid_fee_percentage
rule: "fee_percentage >= 0 AND fee_percentage <= 0.5"
impact: high
- name: valid_settlement_period
rule: "settlement_period IN (0, 1, 2, 3)"
impact: high
comment: "Settlement periods: 0=same-day, 1=next-day, 2=two-day, 3=three-day"
- name: mcc_format_if_merchant
rule: "counterparty_type != 'MERCHANT' OR merchant_category_code LIKE '[0-9]{4}'"
impact: medium
Access Policy:
default: deny
approval_required: true
approval_sla_hours: 4
columns:
- name: credit_limit
classification: confidential
masked_for_roles: [contractor, external_analyst]
mask_strategy: full_mask # Don't show limits to non-authorized users
- name: fee_percentage
classification: confidential
masked_for_roles: [contractor]
mask_strategy: full_mask
- name: counterparty_name
classification: public
masked_for_roles: []
Downstream Consumers:
graph TD
A["transaction-processing Real-time"]
A --> A1["Consumes: Kafka topic counterparties-raw hot path"]
A --> A2["Purpose: Route transactions to correct processor"]
A --> A3["SLA: < 100ms lookup"]
B["settlement-engine Batch"]
B --> B1["Consumes: Iceberg table counterparties.merchants"]
B --> B2["Purpose: Apply correct settlement terms and fees"]
B --> B3["Frequency: Daily"]
C["analytics-reporting Batch"]
C --> C1["Consumes: Iceberg table"]
C --> C2["Purpose: Counterparty performance analysis"]
C --> C3["Frequency: Weekly/monthly"]
Ingest Pipeline¶
# From domains/counterparties/ingest/ingest_job.py
class CounterpartyIngestJob:
DOMAIN = "counterparties"
SOURCE_TOPIC = "counterparties-raw"
TARGET_TABLE = f"{DOMAIN}.merchants"
def run(self) -> None:
try:
self.catalog.create_namespace(self.DOMAIN)
df = self.spark.readStream.format("kafka") \
.option("kafka.bootstrap.servers", self.kafka_brokers) \
.option("subscribe", self.SOURCE_TOPIC) \
.option("startingOffsets", "latest") \
.load()
schema_str = '''{
"counterparty_id": "string",
"counterparty_type": "string",
"counterparty_name": "string",
"merchant_category_code": "string",
"credit_limit": "double",
"settlement_period": "int",
"fee_percentage": "double",
"status": "string"
}'''
parsed_df = df.select(
from_json(col("value").cast("string"), schema_of_json(schema_str))
.alias("data")
).select("data.*")
query = parsed_df.writeStream \
.format("iceberg") \
.mode("append") \
.option("checkpointLocation", f"/tmp/checkpoint/{self.DOMAIN}") \
.toTable(self.TARGET_TABLE)
query.awaitTermination()
except Exception as e:
logger.error("Ingest job failed", error=e)
raise
Governance & Compliance¶
Retention Policy¶
graph TD
A["Counterparty Master: 2 years"]
A --> A1["Rationale: Reference data, inactive counterparties archive"]
A --> A2["Retention starts: From last transaction3-year rule: if no activity, mark inactive"]
A --> A3["Deletion process: Hard delete after 2 years of inactivity"]
Approval Workflow for Commercial Data¶
graph TD
A["User: I need counterparty credit limitsto audit exposure"]
A --> B["OPA Evaluation"]
B --> B1["User role: auditor"]
B --> B2["Data classification: confidential credit_limit"]
B --> B3["Auto-approve? NO"]
B --> B4["Route to: Counterparty Domain Ownertransactions@chakra.fintech"]
B --> B5["SLA: 4 hours"]
B --> C["Owner decision"]
C --> C1["Context review: Purpose? Justification?"]
C --> C2["Accept: Grant access masking rules may still apply"]
C --> C3["Reject: Deny explain why"]
Queries¶
Find High-Risk Merchants¶
-- Merchants with high credit limits and recent suspicious activity
SELECT
c.counterparty_id,
c.counterparty_name,
c.credit_limit,
c.status,
COUNT(t.transaction_id) as transaction_count_24h,
SUM(t.amount) as total_amount_24h,
AVG(r.fraud_score) as avg_fraud_score
FROM counterparties.merchants c
LEFT JOIN transactions.raw_transactions t
ON c.counterparty_id = t.merchant_id
AND t.booking_timestamp > UNIX_TIMESTAMP() - 24*60*60
LEFT JOIN risk_compliance.fraud_scores r
ON t.transaction_id = r.transaction_id
WHERE c.credit_limit > 50000.00
AND c.status = 'ACTIVE'
GROUP BY c.counterparty_id, c.counterparty_name, c.credit_limit, c.status
HAVING avg_fraud_score > 0.5
ORDER BY avg_fraud_score DESC;
Settlement Reconciliation¶
-- Reconcile settled amounts against counterparty master terms
SELECT
c.counterparty_id,
c.counterparty_name,
c.settlement_period,
c.fee_percentage,
COUNT(t.transaction_id) as transaction_count,
SUM(t.amount) as gross_amount,
ROUND(SUM(t.amount) * (c.fee_percentage / 100.0), 2) as fees,
ROUND(SUM(t.amount) * (1 - c.fee_percentage / 100.0), 2) as net_settlement
FROM counterparties.merchants c
INNER JOIN transactions.raw_transactions t
ON c.counterparty_id = t.merchant_id
AND t.status = 'CLEARED'
WHERE CURRENT_DATE - INTERVAL t.booking_timestamp < c.settlement_period
GROUP BY c.counterparty_id, c.counterparty_name, c.settlement_period, c.fee_percentage;
Comparison: Reference Data vs Event Data¶
| Aspect | Counterparties (Reference) | Transactions (Events) |
|---|---|---|
| Change frequency | Hourly to daily | Real-time |
| Size per update | 1-100 records | 1 record |
| Query patterns | Point lookups (merchant details) | Time-range scans |
| Retention | 2 years | 7 years |
| Freshness SLA | 60 minutes | 5 minutes |
| Partitioning | [year, month] | [year, month, day, account_id] |
| Snapshot frequency | Hourly | Every 5 min |
Scaling¶
Kafka topic: counterparties-raw
├── Partitions: 1 (very low volume, < 100 updates/hour)
├── Replication: 3
└── Retention: 2 days
Spark job: CounterpartyIngestJob
├── Executors: 1 (minimal parallelism needed)
├── Cores: 1
├── Memory: 1GB
└── Batch size: Typically 10-50 records per hour
Iceberg optimization:
├── Small table (< 10GB total)
├── Simple partition scheme [year, month]
└── Compaction: Yearly (consolidate annual files)
Next¶
- Market Data Domain — High-frequency pricing feeds
- Transactions Domain — Reference implementation (compare velocities)