Services¶
The 34 wired nano-services and how to wire a custom service into
the runtime. Each service is a small Core subclass that subscribes
to event types and emits its own.
The 34 wired services¶
| Service | Purpose | Subscribed events |
|---|---|---|
mechanism |
Delegation state machine (the heart of the platform) | command events |
audit |
PII-redacted event ledger | every event type (mirror) |
risk |
ML risk scoring | loan.application.received |
fraud |
Fraud detection | loan.application.received, loan.originated |
compliance |
KYC / AML | kyc.check commands |
pricing |
RBI-aligned pricing | pricing.compute commands |
kfs |
Key Fact Statement | kfs.generate commands |
consent |
DPDPA consent lifecycle | consent.record / consent.withdraw |
dsr |
Data Subject Rights | dsr.request |
credit_bureau |
CIBIL / Experian / Equifax + CKYC | credit_bureau.check |
razorpay |
Payment gateway | payment.create, refund.create |
underwriter |
Rule engine + risk model | underwriter.evaluate |
decision |
Final decision | decision.request |
npa |
SMA / NPA / DLG classification | loan events |
collateral |
Collateral mark / liquidate | loan events |
recovery |
Default recovery | default.occurred |
governance |
Governance proposals | proposal commands |
notification |
Multi-channel notifications | fraud.alert, npa.* events |
reporting |
Report generation | reporting commands |
document |
Document generation | document commands |
disbursement |
Disbursement processing | approval events |
collection |
Collection updates | repayment events |
settlement |
Settlement completion | settlement events |
origination |
Origination creation | application events |
servicing |
Loan servicing | loan events |
payment |
Payment handling | payment events |
communication |
Communication dispatch | notification events |
workflow |
Workflow management | workflow commands |
fee |
Fee assessment | loan events |
statement |
Statement generation | statement commands |
prepayment |
Prepayment processing | prepayment commands |
graph |
Delegation graph operations | graph commands |
identity |
Identity management | identity commands |
Plus four KYC provider clients: pan, aadhaar, cibil, ckyc.
The full count is 34 wired services plus 4 providers.
The Core base class¶
Every service extends Core:
class MyService(Core):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.subscribe("loan.originated")
def handle(self, event: Message) -> None:
# domain logic — called by dispatch
...
self.emit("downstream.event", result_payload)
The base class wires:
authz.assert_verified(event)before invoking the handlerbus.idempotency.is_duplicate(...)before invoking the handlertracer.start_span(...)around the handlermetrics.increment(...)after the handler succeedsmetrics.timer(...)for handler latencysupervisor.record_success(...)after the handler succeedsbus.dlq.put(...)on handler exceptioncircuit_breaker.record_failure(...)on handler exception
You write the domain logic; the base class wires the guarantees.
The Emitter helper¶
self.emit(event_type, payload) creates a Message, signs it with
the service's Ed25519 key, and publishes it on the bus. You do not
see any of this — the base class abstracts it away.
class PricingService(Core):
def handle(self, event):
# ... compute pricing ...
self.emit("pricing.computed", {
"loan_id": event.payload["loan_id"],
"apr": 0.289,
"emi": 16543.20,
# ...
})
The StatefulService base class¶
For services that need durable state:
from underwrite.services.base import StatefulService
class AuditService(StatefulService):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.subscribe("every.event.type")
def handle(self, event):
sanitized = redact_event(event)
self.set(f"audit:{event.event_id}", sanitized.to_dict())
StatefulService adds set, get, and delete methods backed by
the configured store. The base class also acquires a per-instance
lock around the handler so concurrent events on the same key are
serialised.
The Dependencies bundle¶
For services with many collaborators, use Dependencies:
from underwrite.services.base import Dependencies
deps = Dependencies(
identity=keypair,
bus=runtime.bus,
store=runtime.store,
metrics=runtime.metrics,
health=runtime.health,
authz=runtime.authz,
tracer=runtime.tracer,
saga=runtime.saga,
supervisor=runtime.supervisor,
secrets_manager=runtime.secrets,
max_concurrent=4,
)
service = MyService.from_dependencies(name="my_service", deps=deps)
Subscriptions and routing¶
A service declares its interests in __init__:
The base class registers each subscription with the bus. The runtime fires events on the bus; the bus dispatches them to every subscriber; the base class handles authz and idempotency before the handler runs.
Service lifecycle¶
class MyService(Core):
def start(self):
# called by the runtime when the service is started
self.running = True
def stop(self):
# called by the runtime on shutdown
self.executor.shutdown(wait=True)
for sid in self.subscriptions:
self.bus.unsubscribe(sid)
Core.stop is the default and handles executor shutdown and
subscription cleanup. Override start if your service needs to do
initialization that requires the runtime to be wired (e.g.,
opening a network connection).
Wiring a custom service¶
To wire a custom service:
- Define the service — subclass
Core(orStatefulService). - Declare subscriptions — call
self.subscribe(event_type)in__init__. - Implement
handle— receive aMessage, do your domain logic, optionally callself.emit. - Register with the runtime — call
runtime.register(name, instance)orruntime.register(name)to instantiate by name. - Start the runtime —
runtime.start([name, ...]).
The runtime will:
- Trust the service identity with authz.
- Register the subscriptions with the bus.
- Attach the metrics, tracer, and supervisor.
- Inject the bus, store, and other dependencies.
What you should not do¶
- Don't import other services. Services compose through the bus, not through direct calls. If you find yourself importing another service, refactor to publish an event instead.
- Don't write the dispatch logic yourself. The base class handles it; writing your own bypasses authz, idempotency, and tracing.
- Don't catch broad exceptions in handlers. Catch what you can handle; let everything else propagate to the DLQ.
- Don't disable the audit service. Every event should be audited.
See also¶
- Architecture — where services fit in the layered design.
- Configuration — the per-service config block.
- Events — the event envelope services send and receive.