VOOZH about

URL: https://dev.to/sujankim/i-published-nepals-first-java-payment-library-to-maven-central-heres-what-broke-1nm7

⇱ I Published Nepal's First Java Payment Library to Maven Central — Here's What Broke - DEV Community



A few days ago, I wrote about building NepalPay — an open-source Spring Boot starter for Nepal's payment gateways (Khalti, eSewa, ConnectIPS, and Fonepay).

That post ended with a roadmap:

Khalti Refund API 🔲 Planned
Retry with Backoff 🔲 Planned
Maven Central 🔲 Planned

All three are now done.

Khalti Refund API ✅ v0.5.0
Retry with Backoff ✅ v0.6.0
Maven Central ✅ v1.0.0

This post is the honest account of how I got there — including every mistake, every failed deployment, and what I wish someone had told me before I started.


NepalPay Is Now on Maven Central

<dependency>
 <groupId>io.github.sujankim</groupId>
 <artifactId>nepal-pay-spring-boot-3-starter</artifactId>
 <version>1.0.0</version>
</dependency>

No repositories block.

No JitPack.

Just:

mvn dependency:resolve

and you're done.


💳 The Thing Nobody Told Me About Khalti Refunds

When I started building the refund API, I assumed it would use pidx — the identifier I already stored from payment initiation.

It does not.

Khalti refunds use transaction_id.

A completely different identifier.

One that only exists after a payment reaches Completed status.

You get it from:

lookupPayment(pidx).transactionId()

What you might try first:

khaltiClient.refundPayment(pidx); // ❌ WRONG

What you actually need:

KhaltiLookupResponse lookup =
 khaltiClient.lookupPayment(pidx);

khaltiClient.refundPayment(
 lookup.transactionId()
); // ✅ CORRECT

Then I found the second surprise.

The refund endpoint has a completely different URL path:

Initiate: https://dev.khalti.com/api/v2/epayment/initiate/
Lookup: https://dev.khalti.com/api/v2/epayment/lookup/
Refund: https://dev.khalti.com/api/merchant-transaction/{transaction_id}/refund/

Notice the refund path has no /api/v2.

It is a different API tree entirely.

I ended up adding:

private final String baseUrl;
private final String baseDomain;

just to construct refund URLs correctly.

NepalPay now supports both:

// Full refund
khaltiClient.refundPayment(
 lookup.transactionId()
);

// Partial refund
khaltiClient.refundPayment(
 lookup.transactionId(),
 5000L
); // NPR 50

🔁 Why I Added Retry — and Why It Defaults to Off

nepalpay:
 khalti:
 retry:
 enabled: true
 max-attempts: 3
 initial-delay-ms: 500
 multiplier: 2.0
 max-delay-ms: 5000

With this configuration:

  1. Wait 500ms
  2. Retry
  3. Wait 1000ms
  4. Retry
  5. Wait 2000ms
  6. Throw an exception

I made retry opt-in deliberately.

Libraries should not silently change the response-time characteristics of existing applications.

If retry was enabled automatically, upgrading NepalPay could suddenly make API calls take several seconds longer.

Opt-in means developers decide when they are ready.

I also had to deal with a distributed systems problem called the thundering herd.

A thousand clients retrying at exactly the same millisecond can keep a failing gateway permanently overloaded.

The fix is jitter.

public static long jitter(long delayMs) {
 if (delayMs <= 0) return 0;

 long range = (long) (delayMs * 0.1);
 long offset =
 (long) ((Math.random() * 2 * range) - range);

 return Math.max(0, delayMs + offset);
}

500ms becomes somewhere between:

450ms <-> 550ms

All clients retry at slightly different times.

The gateway gets a spread of traffic instead of a spike.

It can recover.

Never retry 4xx errors.

A 401 Unauthorized means your secret key is wrong.

Retrying it three times changes nothing.

Only:

  • 5xx server errors
  • Network timeouts

are retried.

Fonepay has no retry at all.

It makes zero server-to-server HTTP calls.

There is nothing to retry.


📦 Maven Central: Five Failed Deployments

Getting onto Maven Central was much harder than I expected.

Mistake #1 — OSSRH Is Dead

Every guide from 2022 told me to use:

nexus-staging-maven-plugin

It failed immediately.

OSSRH was sunset on June 30, 2025.

The new world is:

<plugin>
 <groupId>org.sonatype.central</groupId>
 <artifactId>central-publishing-maven-plugin</artifactId>
</plugin>

Any guide older than mid-2025 is outdated.


Mistake #2 — The Parent POM Chicken and Egg

nepal-pay-parent
├── nepal-pay-core
├── boot3-starter
└── boot4-starter

Maven Central tried to resolve the parent POM.

But the parent had never been published.

Result:

Failed to associate file with coordinates...

Twenty-four times.

The fix:

Make nepal-pay-core completely standalone.


Mistake #3 — The Effective POM Is Not Your pom.xml

I kept looking at my source POM.

That wasn't the file being published.

Maven publishes the effective POM.

The fix:

<plugin>
 <groupId>org.codehaus.mojo</groupId>
 <artifactId>flatten-maven-plugin</artifactId>
 <version>1.6.0</version>
</plugin>

flattenMode=ossrh solved the problem immediately.


Mistake #4 — GitHub Actions Credential Injection

I accidentally overwrote a perfectly valid settings.xml.

I also tried:

${env.CENTRAL_TOKEN_USERNAME}

inside the file.

Those placeholders stayed as literal strings.

Every deployment returned:

401 Unauthorized

The fix:

Let actions/setup-java handle everything.


Mistake #5 — GPG Import with echo Loses Newlines

echo "${{ secrets.GPG_PRIVATE_KEY }}" |
gpg --batch --import

Result:

gpg: no valid OpenPGP data found.

The key was corrupted.

The fix:

gpg-private-key:
 ${{ secrets.GPG_PRIVATE_KEY }}

inside actions/setup-java.

No manual import.

No corruption.


🎉 The Result

Today NepalPay has:

  • ✅ Khalti
  • ✅ eSewa
  • ✅ ConnectIPS
  • ✅ Fonepay
  • ✅ Refund support
  • ✅ Retry with exponential backoff
  • ✅ Spring Boot 3.2+
  • ✅ Spring Boot 4.x
  • ✅ Maven Central publishing
  • ✅ 350+ tests

And developers can integrate Nepal payments with:

KhaltiInitiateResponse response =
 khaltiClient.initiatePayment(request);

return response.paymentUrl();

instead of hundreds of lines of HTTP and cryptography boilerplate.


🔗 Links

GitHub

https://github.com/sujankim/nepal-pay-spring-boot-starter

Maven Central

https://central.sonatype.com/search?q=nepal-pay

Documentation

https://sujankim.github.io/nepal-pay-spring-boot-starter/


If NepalPay saves you time, a ⭐ on GitHub helps other Nepali developers discover it.

Found a bug? Open an issue.

Want to contribute? Open a PR.

Built with ❤️ for Nepal's developer community 🇳🇵