DEV Community

Sujan Lamichhane
Sujan Lamichhane

Posted on

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
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.

Top comments (0)