Overview
Pethotel.io started with a very basic MVP which used to gain real feedback from users. The checkout process was initially made on hypothesis which many turned out be false. Users were dropping off during the payment stage, finding the process confusing or otherwise concerning.
As there were enough evidence to suggest that the checkout process was a bottleneck, we decided to enhance our checkout system to provide a more straightforward experience targeting to solve real observed pain points from gained feedback.
Objectives
- Allow users to save their payment details for future bookings without storing any sensitive card information ourselves.
- Transparent price breakdown to show users a clear breakdown of the total price, including any additional fees or taxes.
- Save incomplete bookings to allow users to complete their bookings later without losing their progress.
- Reduce user confusion by more consistent layout and clear instructions throughout the checkout process.
- Provide more flexible payment options to accommodate various preferences and ease of use.
- Possibility for booking multiple daycare days at once to simplify the process for users with recurring daycare needs. This was a highly requested feature from our users.
Background
At Pethotel.io, we started with a very basic Minimum Viable Product (MVP) and a minimal checkout process. This initial setup was designed to solve observed pain points based on real feedback gained from our operations. Our primary focus was on addressing problems that occurred repetitively, ensuring that the essential needs of our users were met effectively.
Additionally we needed to know exactly where users were dropping off during the payment stage to solve core bottlenecks. They found the process confusing and were concerned about the security of their payment information. These recurring issues were impacting their trust and satisfaction with our service. Recognizing the importance of addressing these challenges, we decided to enhance our checkout system to provide a more straightforward and secure experience.
We needed a system that effectively resolved these repetitive problems, capable of handling the necessary payment methods while providing a secure and seamless user experience. By focusing on practical solutions to these issues, we aimed to enhance customer satisfaction and improve the overall booking process.

Chasing product-market fit
Solving the core flow as well as possible
Denied focus on features that were not directly related to the core flow that proves product-market fit
Pethotel.io decided to focus on the core flow of the checkout process, prioritizing features that directly impacted the user experience. This approach allowed us to address the most critical pain points and streamline the booking process effectively.
Iterative development based on real feedback
Pethotel.io had adopted an iterative development approach, making incremental improvements based on user feedback. This allowed us to refine the checkout system over time, ensuring that it met the evolving needs of our users.
Start from low hanging fruits
Pethotel.io team addressed the most common issues that users faced during the checkout process. By focusing on low-hanging fruits, we were able to make quick wins that improved the user experience and built trust in our service.
Implementation
Approach
Ruby on Rails was already used as back end which provided a starting point to build further on. Stripe was also already in use.
The MVP checkout was build upon html.erb templates using stimulus controllers for minimal interactivity.
When it comes to front-end, stimulus and html.erb have both been great tools for doing things fast but as our customer needs turned out to be complex, we had to build a more interactive and dynamic system to handle these things in predictable and understandable way.
I have been previously struggling with the complexity of managing state in React applications. I knew beforehand that this would be absolute mess to handle by passing props around and trying to keep track of everything. I decided to use Redux for state management targeting to keep things organized and understandable.
Redux was used to track states such as user details, selected pets, selected dates, and payment methods. This allowed us to manage the user's progress through the checkout process effectively.
State tracking also paid important role in form validations since for example application allows user to pay now or pay later but not all payment methods are available for both options. As a result we needed to keep track of the selected payment method and validate it against the selected payment option. Then do required actions to automatically switch payment method if needed.
- Ruby on Rails as the backend framework for all the server work.
- Stripe was used for all payment related actions.
- Redux for state management to track user progress and manage form validations.
- Debounced autosave to autosave progress without overwhelming the server.
- React for the user interface

Single Source of Truth
-
I broke the BookingPage.tsx to smaller components, each responsible for their own thing in the page. Then made them to be rely on the global redux state to pursue single source of truth principle and avoid prop drilling. I created components likeBookingPageDateAndGuests
,BookingPageOrderFields
, andBookingSubmitButton
which helped me to understand code better. From my experience, shorter components also be in you favor when prompting to ai tools like chatGPT as they usually provide better & more focused answers.
For instance,BookingPageDateAndGuests
handles date selection and guest information, allowing user to pick dates and add, select or remove pets. Calendar & pet related forms had global components in place already so I just had to make sure that the selected dates and pets were selected and stored in the global state aswell as displayed to the user as such.BookingPageOrderFields
was managing user inputs like name and contact information, whileBookingSubmitButton
deals with form validation and submission logic. By isolating functionality into these components, I could focus on each piece individually without worrying about breaking other parts of the app. This really paid off here since there were such a heavy requirements for the checkout.
- I tried relying on existing/reusable components as much as possible to save time and to avoid reinventing what's already working & tested. For all the modal windows, date pickers, form fields, buttons and other commonly used things I tried to either use existing components or create new ones that could be used many times over. I personally haven't experienced building reusable components taking more time compared to building them for single use only.
-
Multi-Day Booking Capability
Users frequently requested the ability to book multiple daycare days in one go. Implementing this feature streamlined the process for those with recurring needs, making our service more convenient and aligning with what our customers were asking for.
The
BookingPageCalendar
component was enhanced to allow users to select multiple dates easily. It interfaces with the backend to fetch availability and reflects disabled dates where bookings aren't possible, improving the overall booking experience.
Field Validation and User Feedback
-
Real-Time Form Validation and User Feedback
Nothing's more frustrating than filling out a form only to find out you've made errors after submitting. I added real-time validation to guide users as they fill out the form, reducing mistakes and making the process more user-friendly. This is also something we already had received countless evidence from our users that the validation was not always clear or working as expected.
The
BookingSubmitButton
component includes validation logic that relies on the global state to check for errors and provide feedback to the user. This approach ensures that users are aware of any issues before submitting their booking, reducing errors.

Autosave Debounced
We noticed that quite many of our users did not complete the booking process in one go. A past issue we faced was users losing their progress if they navigated away from the checkout page.
Initially, we had a quick fix that saved values only at a specific point in the checkout process, rather than always being up-to-date.
Since I wanted to autosave their data as they typed but didn't want to hammer the server with requests every time a user made a keystroke, I implemented a debounced autosave. It triggers when major actions happen, such as when typing stops.
Debounced autosave waits for the user to stop typing for a set amount of time. If the user continues typing, the timer resets. This approach minimizes unnecessary server calls while ensuring the user's progress isn't lost.
It turned out to work surprisingly well and was also surprisingly easy to implement at least by using the commonly used lodash library. It was my first time creating an autosave feature that handled so many fields by passing the state values to the debounced function.
const saveBookingDebounced = debounce(
(dispatch: AppDispatch, stateSnapshot: BookingState, orderId: string | null) => {
if (orderId) {
dispatch(
saveBookingData({
orderId,
data: {
email: stateSnapshot.email,
first_name: stateSnapshot.firstName,
last_name: stateSnapshot.lastName,
phone_number: stateSnapshot.phoneNumber,
comment: stateSnapshot.comment,
selected_card_id: stateSnapshot.selectedCard,
selected_pets: stateSnapshot.selectedPets,
selected_dates: stateSnapshot.selectedDates.map((date) => {
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
return localDate.toISOString().split("T")[0];
}),
},
})
);
}
},
1000
);

Checkout Backend
The backend was responsible for handling payment processing, order creation, and other needed data management. Some controller methods like price calculation and order creation turned out quite heavy so I decided to delegate them to dedicated services files to maintain focus.
Price calculation is actually hard to get right and it's very easy to make mistakes at least for me. I had to calculate the price based on the selected dates and services. I also had to consider any additional fees, VAT's or discounts that might apply. I created a PriceCalculationService
in order to understand the logic better and to make it easier to debug.
Currency conversion was out of scope for this project but it's something that I would have to consider once the service would be used in multiple countries.
What makes price calculation so difficult?
- It's such a boring task that it's easy to make mistakes without noticing.
- It might be time consuming to debug and understand the logic behind it.
- Many edge cases to consider, such as services, discounts, VAT's, and additional fees, cents to eur conversion etc.
- It being seemingly simple may trick for overconfidence causing to forego testing.
You can write tests for sure but you won't get away from putting thought to it at some point.
class PaymentMethodService
def initialize(user, payment_method_id = nil, order_id = nil)
@user = user
@payment_method_id = payment_method_id
@order_id = order_id
Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
end
def add_payment_method_with_setup_intent
stripe_customer = find_or_create_stripe_customer(@user)
setup_intent = create_setup_intent(stripe_customer.id)
{ setup_intent_client_secret: setup_intent.client_secret }
rescue => e
{ error: e.message, status: 400 }
end
end
class OrderCreationService
def initialize(user:, incomplete_order:)
@user = user
@incomplete_order = incomplete_order
@hotel = Hotel.find(incomplete_order.hotel_id)
@service = HotelService.find(incomplete_order.hotel_service_id)
@price_cents = (incomplete_order.incomplete_order_price.to_be_paid_later * 100).to_i
@comment = incomplete_order.comment
@commission_amount_cents = (incomplete_order.incomplete_order_price.user_commission_amount * 100).to_i
end
def process_stripe_payment_intent_order(payment_intent_id)
update_incomplete_order_to_paid
update_user_with_incomplete_order_fields
order = create_new_pending_order
room = create_or_find_room_for_order(order)
save_payment_intent_id_to_order(order, payment_intent_id)
handle_order_pets(order)
associate_order_with_room(order, room)
create_message_for_order(order, room)
add_incomplete_order_comment_to_room_as_message(room)
send_to_zapier(order, "success")
enqueue_notification_job(order)
room
rescue => e
{ error: e.message, status: 400 }
end
end
Authorize Payment Method for later without charging anything at first
One of the key features we needed was the ability for users to save their payment details for booking without charging them immediately. This requires 3D Secure authentication to authorize the payment method for future use.
Why this was so important?
When users initiate contact with pet sitter, it is not often fully certain if the booking will happen. We wanted to provide our users the ability to contact pet sitters without having to pay anything at first. This way they could be sure that the pet sitter is available and willing to take care of their pet before committing to anything.
Additionally some of our users initiate contact with multiple pet sitters at once to make sure that at least some will be a fit. So if charging would happen at first, it would reserve unnecessary funds from customers accounts from booking that might not ever happen.
Challenges Faced
Building a checkout like this came with it's challenges. Here are some of which were tackled...
Pet sitting is complex service to understand from the user perspective
Pet sitting is a complex service that requires understanding on how the user thinks and what they truly need. The booking process needed to be intuitive and user-friendly to cater to a diverse range of users. It also had to be guided enough to build trust in the service.
State Management Complexity
Managing the state of user interactions during payment was challenging. There were many moving parts, such as user details, selected pets, dates, preferences and payment methods, that needed to be tracked and validated throughout the process. For example checkout allowed choosing to pay now or pay later but not all payment methods were available for both options. This required handling state's so that it recognizes the selected payment method and validates it against the selected time of payment.
Many Edge Cases to Handle
Addressing edge cases such as failed payments, incomplete orders, and simultaneous transactions demanded robust error handling and fallback mechanisms.
I tried to handle error handling routines within services to manage different failure scenarios gracefully. Implementing fallback mechanisms ensured the system remained stable even when things didn't go as planned. Regularly testing these edge cases helped refine the approach and maintain data integrity.
Allowing users to pick many daycare periods at once required business logic changes
Users frequently requested the ability to book multiple daycare days in one go due to the nature of the service. Implementing this required changes to the backend to handle multiple date ranges simultaneously. This involved updating the booking logic to accommodate multiple dates and services, ensuring that the system could process these requests accurately everywhere in the app, from receipts to availability handling and much more.
Lessons Learned
Time Investment
Building software like this takes time and effort.
Build upon real feedback not assumptions
Real user feedback is invaluable in identifying pain points and guiding development. There were many surprises and things that we thought were working but turned out to be false assumptions. This also reduces the risk of building features that users don't need or want.
Modularity helps
More modular approach helps to maintain and understand the system. Breaking down complex functionalities into discrete services allowed for easier updates and feature additions without disrupting existing workflows.
State Management is Key
Global state management helps when there are many moving parts to track. Redux was a powerful tool for managing user progress and form validations, ensuring that the checkout process was smooth and error-free.
The Advanced Checkout System was a significant improvement over the previous checkout process. By focusing on real user feedback and pain points, we were able to streamline the booking process and increase user satisfaction. The system's flexibility and modularity allowed for easy updates and feature additions, ensuring that it could adapt to changing user needs.
The results speak for themselves, with increased conversion rates and improved user experience. By investing time and effort into building a robust checkout system, we were able to provide a more efficient and user-friendly experience for our customers.