Case study - payments
How GolfNext stopped losing checkouts at peak payment load
At low traffic, payments worked. At peak, exactly when the most money was moving, they did not. Mavka traced it to one quiet backend decision: payment waiting lived inside worker threads.
Key results
~10x
peak payment load
BeforeCheckouts started failing at ~100 payments/min.
After~1,000 payments/min sustained on the same flow.
0
new servers added
BeforeMore hardware looked like the obvious answer.
AfterThe same backend capacity worked after the wait moved out of threads.
Minutes
deploys and rollback
BeforeLong polling drains slowed instance replacement.
AfterReleases stopped waiting on idle payment checks.
The problem
Around a hundred payments a minute, checkout started failing outright.
Customers scanned a QR code, paid with MobilePay, Vipps, or Swish, and waited. Under normal traffic it worked. Around 100 payments a minute, new payments started failing.
It looked like a flaky payment provider or an undersized backend. It was neither.
Scan QR code
Customer starts checkout at the venue
Create payment order
GolfNext opens a payment session
Confirm in mobile app
MobilePay, Vipps, or Swish handles approval
Wait for confirmation - the break point
The backend used to hold a worker thread here
Why it was hard to see
The waiting was happening in the wrong place.
The backend used blocking polling: after creating a payment, it held a worker thread and repeatedly checked the provider until the customer confirmed or the flow timed out.
When enough payments were pending at once, the thread pool filled with payments doing almost nothing. New payments had no thread to run on, so they failed even though the backend was barely working.
What Mavka changed
Two steps: contain the damage, then remove the cause.
Step 01 - contain
Isolate payments onto their own thread pool
A payment spike could no longer starve the rest of the backend. The pool still filled with idle waits, but the damage stopped spreading.
Step 02 - cure
Replace polling with an SQS delayed-message loop
Instead of holding a thread while waiting, the service queues a short check-payment-status task with a 3-5s delay and releases the thread.
Before, one thread served one payment. Now it serves about ten.
Picture a worker thread as a call-center operator. Blocking polling kept an operator on the line for the whole payment — mostly listening to hold music, unable to take another. Moving the wait into an SQS queue frees the operator between checks, so the same one clears roughly ten payments in that window.
One operator, stuck on one call
A new payment gets a busy line. = the held thread can’t accept it
The payment isn’t even done yet, but the operator is already tied up — so the next customer can’t get through.
One operator, a queue that calls back
Same window, about ten payments cleared. = thread released between checks
Nobody holds the line for silence — the queue rings back when it’s time to act.
A held thread can’t accept a new payment
One thread handles several payments
"Mavka did not just throw servers at it. They found the real cause and rebuilt the flow. The same setup now handles roughly ten times the load."
- Peter Selmer, CEO, GolfNext
If this sounds familiar
You do not need to know the root cause yet.
These problems usually arrive as messy product symptoms first: failed checkouts, long waits, confused customers, and peak periods the system cannot quite absorb.
- 01Payments sometimes disappear, fail, or need manual follow-up.
- 02Customers wait too long after confirming a payment in the app.
- 03Checkout works on a normal day, but breaks when venues are busiest.
- 04Support hears "I paid, but nothing happened" more often than it should.
- 05The team keeps adding capacity, but the peak-load problem comes back.
Fix a revenue-critical system
We fix the engineering problems that block revenue: payment reliability, backend scalability, cloud cost, release safety, and senior architecture work that turns unclear failures into operable systems.
Book an audit