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
Enter fullscreen mode Exit fullscreen mode
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 ✅
Enter fullscreen mode Exit fullscreen mode
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.duration
nepalpay.khalti.payment.lookup.duration
nepalpay.khalti.payment.refund.duration
nepalpay.esewa.callback.verify.duration
nepalpay.esewa.status.check.duration
nepalpay.connectips.validate.duration
Enter fullscreen mode Exit fullscreen mode
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?
Enter fullscreen mode Exit fullscreen mode
histogram_quantile(
0.99,
rate(nepalpay_khalti_payment_initiate_duration_seconds_bucket[5m])
)
Enter fullscreen mode Exit fullscreen mode
Retry Counters
Retries are now measurable too.
nepalpay.khalti.retry.attempts
Enter fullscreen mode Exit fullscreen mode
One interesting bug appeared during development.
Originally all reactive retry paths shared one helper:
metrics.incrementInitiateRetry();
Enter fullscreen mode Exit fullscreen mode
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.failed
nepalpay.fonepay.callback.signature.failed
Enter fullscreen mode Exit fullscreen mode
Suddenly these become security alerts instead of silent failures.
Example Grafana alert:
rate(nepalpay_esewa_callback_signature_failed_total[5m]) > 5
Enter fullscreen mode Exit fullscreen mode
Actuator Health Indicators
Every configured gateway automatically registers its own health component.
GET /actuator/health
Enter fullscreen mode Exit fullscreen mode
Example:
{
"status": "UP",
"components": {
"nepalpayKhalti": {
"status": "UP",
"details": {
"gateway": "Khalti",
"mode": "SANDBOX"
}
},
"nepalpayConnectIps": {
"status": "UP",
"details": {
"pfxLoaded": true
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
everything now becomes a proper reactive error signal:
return Mono.defer(() -> {
validateRequest(request);
return webClient.post()...
});
Enter fullscreen mode Exit fullscreen mode
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(...));
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
with
MessageDigest.isEqual()
Enter fullscreen mode Exit fullscreen mode
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'
Enter fullscreen mode Exit fullscreen mode
The solution:
Move all metrics classes into
nepal-pay-core
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
to
org.springframework.boot.health.contributor
Enter fullscreen mode Exit fullscreen mode
and split them into a dedicated module.
Boot 4 therefore requires:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-health</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
Zero Configuration
Simply add:
spring-boot-starter-actuator
Enter fullscreen mode Exit fullscreen mode
Everything else configures automatically.
Disable if desired:
nepalpay:
metrics:
enabled: false
health:
enabled: false
Enter fullscreen mode Exit fullscreen mode
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.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.