DEV Community

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

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.failed
  • nepalpay.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() with MessageDigest.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.