Case study · Systems · 2026
The hidden engineering behind a £1 test booking
A few days ago I paid £1 to book an appointment with myself.
It wasn't an especially exciting transaction.
I opened Connection Clinic, found one of my test appointments, entered my details, paid £1 and received the confirmation emails.
The slot disappeared from the booking page and appeared in my Outlook calendar.
Then I cancelled it.
The Outlook event disappeared, the appointment became available again and, after fixing one last problem, Stripe returned my £1.
From the outside, that's roughly what a booking system is supposed to do.
What interested me was how much had to happen behind that £1 for the experience to remain that boring.
What the client sees
The client journey is intentionally uneventful.
Choose an appointment.
Enter your details.
Pay.
Receive confirmation.
That's about it.
But by the time the client reaches Stripe, Connection Clinic has already done considerably more work.
For an Outlook-connected therapist, it has derived possible appointments from their working hours and current calendar commitments.
The appointment the client selects is represented by a signed slot rather than trusting a date and time submitted from the browser.
When they continue, the server regenerates the opportunity and makes sure it still exists.
Outlook is checked again because somebody could have added a calendar event since the booking page was loaded.
Connection Clinic applies its own conflict checks.
A pending booking hold claims the appointment.
A provisional event is written to Outlook.
Only then does the payment flow continue.
What looks like:
choose slot → payis closer to:
availability → signed slot → server revalidation → fresh calendar check → booking hold → exact-slot claim → Outlook event → Stripe CheckoutAnd payment still isn't the end.
Booking is partly a concurrency problem
One of the easiest ways to underestimate booking software is to think of availability as a list of times.
The real problem is that availability changes.
Two people can see the same appointment.
A therapist can put something into Outlook while a client has the booking page open.
A checkout can be started and abandoned.
Two requests can arrive almost simultaneously.
So "3pm is free" isn't enough.
At some point the system needs to turn:
3pm appears freeinto:
3pm now belongs to this bookingwithout accidentally selling it twice.
Connection Clinic already had conflict protection and temporary booking holds. The Outlook work had to join that architecture rather than bypass it.
For external-calendar slots I also added a durable exact-slot claim, alongside the existing conflict checks, to protect the same therapist/date/time combination while a booking is pending or paid.
If the process fails before payment, the system needs to unwind what it has already done.
A provisional Outlook event shouldn't sit in a therapist's calendar forever because somebody opened Stripe and then went to make a cup of tea.
Expired holds therefore need cleanup too.
Payment is another system
Connection Clinic uses Stripe Checkout, with Stripe Connect handling the split between the platform and therapist.
For the £1 test transaction, the important part wasn't the amount. It was exercising the same production pathway a real booking would use.
The payment belongs to a wider lifecycle.
Stripe confirms successful payment asynchronously. Connection Clinic records the payment against the booking and sends the appropriate confirmation emails.
So now the same appointment has state in several places.
Connection Clinic knows there is a booking.
Outlook knows the therapist is busy.
Stripe knows money moved.
Email tells the people involved what happened.
The job isn't merely making each integration work individually.
It's making their combined states make sense.
Cancellation runs the process in reverse
Cancelling a paid appointment initially sounds straightforward too.
Refund the client.
Delete the calendar event.
Cancel the booking.
But with Stripe Connect, a full refund isn't simply sending £1 back.
Connection Clinic uses a destination-charge model. Part of the payment goes to the therapist's connected Stripe account and Connection Clinic retains its platform fee.
For a full therapist-initiated cancellation, the intended result is that nobody keeps their share.
So the refund needs to return the customer's payment, reverse the therapist's transfer and refund Connection Clinic's application fee.
Meanwhile the appointment needs to be cancelled in Connection Clinic, its Outlook event removed and its time released back into availability.
Again, several systems need to converge on the same outcome.
I deliberately made Connection Clinic cancellation authoritative.
The booking is marked cancelled first.
The external cleanup follows.
If Outlook deletion fails, that doesn't mean the client is suddenly still booked.
If Stripe fails, the booking can remain cancelled while the refund is marked as needing attention and retried.
That distinction matters because pretending a multi-system operation is one atomic database transaction doesn't make it one.
Idempotency becomes surprisingly important
Retries introduce another problem.
If a refund request times out, did Stripe receive it?
If the user clicks retry, should we send another £1?
Clearly not.
The refund path therefore uses a stable idempotency key tied to the Connection Clinic booking.
Successful refund metadata is also persisted so future attempts can stop before creating another refund.
The same idea appears elsewhere in the system.
Calendar event creation needs to tolerate retries without creating duplicate appointments.
Booking attempts need durable conflict protection.
Webhook processing needs to be safe when events are delivered more than once.
A lot of reliable software comes down to making "try that again" safe.
Then I broke production with a database migration
The £1 booking also exposed a less glamorous part of building the system.
I'd added the fields Connection Clinic needed to audit Stripe refunds: charge IDs, refund status, refund amount, timestamps, errors and transfer-reversal information.
The code was deployed.
Production then started throwing an error on /book.
The application was querying one of the new refund columns, but the production database didn't have the schema state the new application expected.
A booking feature had managed to break the booking page before anyone even booked anything.
The immediate lesson was obvious: application code and database schema have to be deployed as a pair.
The more useful lesson came from working out how to recover safely.
I didn't want to start manually altering migration history until I understood why production and the migration runner disagreed.
So I inspected the production migration table and schema, created a separate Neon branch from the database, and rehearsed the migration there first.
That revealed an awkward bit of history: development schema pushes had already introduced some of the columns, while the corresponding formal migration had not been recorded.
Fortunately, I'd written the migration defensively. Existing types and columns could be tolerated, missing pieces could be added, and the migration could still become the formal record of the schema change.
Once that behaviour had been proven against the rehearsal database, I ran the same migration process against production and verified that all migrations were recorded.
The booking page came back.
It's not the kind of work anybody sees in the finished product.
It is part of the product nonetheless.
My refund failed successfully
Then came my favourite bug in the whole exercise.
I cancelled the £1 booking and tried the refund again.
Stripe showed the money had been refunded.
Connection Clinic showed the booking as refunded.
And Connection Clinic also displayed:
Refund retry could not be completed. Try again later.Which was confusing, given that it had just completed.
The Stripe integration wasn't the problem.
The redirect was.
The server action performed the successful redirect inside a try block. In Next.js, redirect() works by throwing a special redirect response.
My catch block then helpfully caught the successful redirect and treated it as an error.
So the actual sequence was effectively:
refund succeeds → save success → redirect to success → catch the redirect → redirect to failureEverything important had worked.
The error handling was wrong.
Moving the successful redirect outside the try/catch fixed it.
I like bugs like this because they puncture the idea that the most complicated-looking part of a system is necessarily where the difficult bugs live.
The Stripe Connect refund worked.
A few lines of control flow made it look as though it hadn't.
Then I bought another £1 appointment
After deploying the fix, I tested the pathway again.
Booking worked.
The slot disappeared.
The Outlook event appeared.
Payment completed.
Emails arrived.
Cancellation removed the appointment from Connection Clinic and Outlook.
The refund completed.
And this time the interface agreed.
That final test wasn't really about £1.
It was checking that a chain of independently fallible systems produced one coherent result.
Simple software isn't necessarily simple underneath
I don't think every website needs this kind of engineering.
In fact, one of the reasons we originally used off-the-shelf booking software for Connection Clinic was precisely because building booking and payment infrastructure unnecessarily would have been a poor use of time.
We built our own system when the business eventually gave us a reason to.
And once you own that system, the standard changes.
A client shouldn't have to understand booking holds, Microsoft Graph, Stripe destination charges, idempotency keys, webhook delivery or database migrations.
A therapist shouldn't have to understand them either.
They should see an available appointment.
Someone should be able to book it.
The therapist's calendar should update.
Money should go to the right places.
And if the appointment is cancelled, everything should unwind correctly.
The complexity hasn't disappeared.
It's just been moved somewhere the user doesn't have to deal with it.
I think that's one of the better definitions of good software.
Next
Adding Outlook to a booking system without making Outlook the booking system
How Connection Clinic uses Outlook to inform availability while keeping bookings authoritative inside its own platform.