NepalPay v1.2.0 - Metrics, Health Indicators, and Everything CodeRabbit Caught
My previous article ended with NepalPay being published to Maven Central.
Khalti Refund API β
v0.5.0
Retry with Backoff β
v0.6.0
Maven Central β
v1.0.0
The library worked. Tests passed. Developers could install it with a single dependency. But there was still one major problem.
What happens when something goes wrong in production? Not whether a payment succeeds-that's what lookupPayment() is for. I mean:
- Is the gateway configured correctly?
- Is it running against Sandbox or Production?
- How long are API calls taking?
- How often are retries actually happening?
- Are callback signature failures increasing?
Until now, NepalPay couldn't answer any of those questions. Version 1.2.0 changes that.
Micrometer Metrics β
Health Indicators β
Reactive Improvements β
400+ Tests β
The Problem
Imagine a Khalti payment suddenly starts failing. Without observability you only know one thing: The payment failed. You don't know:
- whether it timed out
- whether retry fired
- how long it took
- whether 1 request failed or every request failed
Production systems need answers.
Micrometer Metrics
NepalPay now records metrics automatically whenever spring-boot-starter-actuator is present. No configuration required.
Every gateway records operation-specific timers.
nepalpay.khalti.payment.initiate.durationnepalpay.khalti.payment.lookup.durationnepalpay.khalti.payment.refund.durationnepalpay.esewa.callback.verify.durationnepalpay.esewa.status.check.durationnepalpay.connectips.validate.duration
Each metric is tagged with:
- gateway
- sandbox/production
- success/error
That means Grafana can immediately answer questions like: What is the P99 latency for Khalti payment initiation?
histogram_quantile(0.99, rate(nepalpay_khalti_payment_initiate_duration_seconds_bucket[5m]))
Retry Counters
Retries are now measurable too.
nepalpay.khalti.retry.attempts
One interesting bug appeared during development. Originally all reactive retry paths shared one helper:
metrics.incrementInitiateRetry();
That meant:
- lookup retries incremented initiate
- refund retries incremented initiate
The metrics were wrong. CodeRabbit spotted it during review. The fix was simple: Pass a retry callback into every operation instead of hardcoding one counter.
Security Metrics
Signature verification failures are now tracked.
nepalpay.esewa.callback.signature.failednepalpay.fonepay.callback.signature.failed
Suddenly these become security alerts instead of silent failures.
Example Grafana alert:
rate(nepalpay_esewa_callback_signature_failed_total[5m]) > 5
Actuator Health Indicators
Every configured gateway automatically registers its own health component.
GET /actuator/health
Example:
{
"status": "UP",
"components": {
"nepalpayKhalti": {
"status": "UP",
"details": {
"gateway": "Khalti",
"mode": "SANDBOX"
}
},
"nepalpayConnectIps": {
"status": "UP",
"details": {
"pfxLoaded": true
}
}
}
}
Notice something: There is no HTTP ping. That was intentional. Sandbox APIs often rate limit. Health checks should verify configuration-not internet connectivity.
Reactive Starter Improvements
The reactive starter shipped in v1.1.0. Version 1.2.0 hardened it.
Every validation step now lives inside Mono.defer(). Instead of throwing exceptions immediately:
validateRequest(request);
everything now becomes a proper reactive error signal:
return Mono.defer(() -> {
validateRequest(request);
return webClient.post()...;
});
This keeps operators like:
onErrorResume()onErrorReturn()retryWhen()
working correctly.
Reactive Timing
Micrometer's traditional timing API is blocking. Reactive applications require a different pattern.
Timer.Sample sample = Timer.start();
return source
.doOnSuccess(v -> sample.stop(...))
.doOnError(e -> sample.stop(...));
No blocking. No scheduler switching. Pure Reactor.
What CodeRabbit Found
I use CodeRabbit on every PR. For v1.2.0 it found 19 issues. The most important ones:
- Retry counters attributed to the wrong operation - Every retry became an initiate retry. Fixed.
- Missing timer inside
verifyCallback()- Internal calls bypassed the public timed method. Status metrics disappeared. Fixed. - Logging decoded callback JSON - Originally:
log.debug(jsonString);That JSON is attacker-controlled. Removed. - Transport failures skipped retry - Network failures were wrapped as generic exceptions. Retries never happened. Now transport failures are caught separately.
- Constant-time signature comparison - Replaced
String.equals()withMessageDigest.isEqual()to avoid timing attacks.
Multi-Module Challenge
One design problem surprised me. Where should the metrics classes live? Originally they lived inside the Boot 3 starter. The reactive starter depended on Boot 3. Spring Boot then reported: Duplicated prefix 'nepalpay'
The solution: Move all metrics classes into nepal-pay-core. Every starter already depends on it. No duplicate configuration. No circular dependencies.
Spring Boot 4.1.0 Health API
Boot 4.1.0 moved health APIs from org.springframework.boot.actuate.health to org.springframework.boot.health.contributor and split them into a dedicated module. Boot 4 therefore requires:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-health</artifactId>
</dependency>
Zero Configuration
Simply add:
spring-boot-starter-actuator
Everything else configures automatically. Disable if desired:
nepalpay:
metrics:
enabled: false
health:
enabled: false
What's Next
Upcoming roadmap:
- ConnectIPS configurable timeout
- Kotlin examples
- eSewa Refund API
- Webhook support
GitHub: https://github.com/sujankim/nepal-pay-spring-boot-starter
Documentation: https://sujankim.github.io/nepal-pay-spring-boot-starter/
Maven Central: https://central.sonatype.com/search?q=nepal-pay
If NepalPay saves you time, consider giving the project a β on GitHub. It helps more Nepali developers discover the library.
Comments
No comments yet. Start the discussion.