Simplifying & securing online payment through all-in-one app

About the product
Condexo was building Condexo Pay, a fintech platform intended to bring everyday payments and services into a single digital experience. The MVP brought together workflows such as utility payments, mobile recharges, gift cards, and payment-related services that were traditionally handled through separate channels.
I joined the project as the frontend developer, working with the existing product architecture and turning the MVP requirements and designs into a reliable, responsive frontend.
The System I was added to
When I joined, the product was already being developed with its core architecture in place. The team consisted of a project manager, a backend developer, and me responsible for the frontend, working alongside the client's stakeholders and in-house designer.
The frontend was built with React, with Redux acting as the central state layer and Redux Saga coordinating asynchronous communication with backend services. In practice, client state and data originating from APIs were both being represented through the same state-management layer.
This architecture provided a solid foundation for the initial product. As more payment services and application workflows were introduced, however, the responsibilities of the state layer became increasingly intertwined. A feature could require additional actions, reducers, selectors, and saga logic even when the underlying data ultimately originated from a backend service.
That led to a more fundamental question: should client state and server state be managed in the same way?
Decisions
1. Separate client state from server state
The first decision was not about replacing Redux with another library. It was about understanding what the application was actually asking its state-management layer to do.
"Should state owned by the frontend and state owned by the backend share the same management model?"
Client state represents decisions and interactions that belong to the frontend: UI state, local selections, and other values controlled by the application. Server state has a different lifecycle. It originates from backend APIs, can be consumed by multiple parts of the application, and needs to account for request, loading, error, caching, and refresh behaviour.
Treating both categories as one problem meant that API interactions had to be represented through the same Redux machinery used for client state. I proposed separating the two responsibilities so that each could be handled according to its actual lifecycle.
2. Client state: Zustand
Once client state was its own concern, the next question was how to manage it without recreating Redux ceremony for values the frontend already owned. The criteria were simple: low boilerplate, incremental adoption in an existing codebase, and a clear boundary from API-derived data.
Redux Toolkit was the safest path because the project already used Redux, but it would still keep client state inside the same store model we were trying to shrink. React Context with useReducer avoided a new dependency, but it did not scale cleanly as more features needed shared UI state. Zustand fit the need better: a small store API that components could use directly, without actions, reducers, or selectors for local application state.
Existing approach: Redux
- Centralized application state
- Actions and reducers around feature state
- Additional selectors where required
- Saga layer for asynchronous behaviour
Proposed approach: Zustand
- Lightweight store for client-owned state
- Less feature-level boilerplate
- Direct access to the state required by components
- Clearer boundary between local state and API data
The important change was not simply reducing the number of files. It was reducing the abstraction required to represent state that was already local to the application. The trade-off was less formal structure than Redux, which was acceptable for the size of the MVP and the kinds of client state we needed to hold.
3. Server state: React Query
For API-derived data, the requirement was different: request lifecycle, loading and error state, caching, and refresh behaviour had to be handled without rebuilding that machinery for every feature. Staying on Redux Saga was possible, but it would have preserved the same multi-file pattern we were trying to escape.
SWR offered a similar mental model with a lighter surface area. RTK Query would have kept fetching inside the Redux ecosystem, which worked against the separation we had just defined. React Query was chosen because it treated server state as its own concern: built-in request lifecycle, cache ownership, and hooks that components could consume without copying backend responses into a general-purpose store.
Redux + Saga
- Explicit actions for requests and responses
- Reducer state for loading and errors
- Saga logic for asynchronous orchestration
- Manual handling of service data lifecycle
React Query
- Built-in request lifecycle
- Loading and error states available to consumers
- Caching of server-derived data
- Data fetching separated from application state
In practice, that difference showed up in the file structure. Each API-backed feature needed its own actions, reducer, saga, and selectors just to move service data through Redux. React Query collapsed that into a query hook and an API call.
This changed the responsibility of the frontend state layer. Instead of copying backend responses into a general-purpose application store and maintaining their lifecycle manually, server data could remain associated with the service that owned it while React Query handled fetching, caching, and the request lifecycle around that data.
The resulting separation gave each state type a clearer responsibility: Zustand managed state the frontend owned, while React Query managed data whose source of truth lived behind the backend APIs. The trade-off was a new dependency and a learning curve for the team, in exchange for removing repeated API plumbing from every feature.
4. Payments: Stripe
Payment processing was a product decision as much as a frontend one. Condexo Pay needed secure card handling for an Italian fintech MVP without taking PCI scope into the application UI. Building directly against bank rails or a lower-level processor would have slowed delivery and pushed sensitive payment details into our frontend surface.
What we needed
- Card payments without storing card data in the app
- A mature JS SDK that fit a React frontend
- Clear payment status and webhook flows
- A path that could ship with the MVP timeline
Why Stripe
- Elements kept card data in Stripe's PCI scope
- Documented APIs and dashboard for payment operations
- Webhooks and status model for payment history UI
- Faster integration than custom processor work
Stripe became the payment layer for card and online payment flows in the product. Domain-specific Italian services such as bollettini still depended on their own backends and partners; Stripe was not treated as a single solution for every payment type, only for the card-processing surface the frontend had to integrate safely and quickly.
5. The resulting frontend architecture
The architectural change was therefore less about introducing two new libraries and more about establishing a clear ownership boundary between two different kinds of state.
With that foundation in place, the frontend could continue expanding across the product's payment and service workflows without forcing every feature to reproduce the same API state-management plumbing.
Outcome
The frontend was delivered as part of Condexo Pay's MVP, supporting a range of payment and service experiences within a single responsive application. The product could bring workflows such as utility payments, mobile recharge, gift cards, and payment history into one customer-facing platform.
Beyond shipping the MVP, the frontend architecture evolved with the product. Separating client state from server state reduced the responsibility placed on a single state-management layer and gave subsequent features a clearer path for handling local interactions and backend data independently.
Testimonials

It was a pleasure to work with Parmeet, an honest and reliable Dev!

I really enjoyed working with Parmeet. Highly recommended for React-based web development projects with the rich user interfaces. He is eager to learn and improve in all areas. He has excellent communication and technical skills. He is hard-working, ensures deadlines are duly met, and never compromises with product quality.

Parmeet is an excellent developer and a great friend, he is really good in React and has been a real gem to our last company. His technical skills are impressive as per his experience. He always makes sure to meet all the deadlines within a given time frame & with the top standards. He is hardworking & dedicated in his work. It is a pleasure and honor to recommend Parmeet to anyone who wants to hire him.

Parmeet is a good and technical sound is his work. I worked with him on 2 projects and he developed the project proficiently on frontend and backend. I hope to get work opportunities with him in future so we can connect the world together for the betterment.
Let's bring your nextbig idea to life
Whether you're brainstorming ideas, looking to bolster capabilities, rapidly expanding your team, or delegating projects, count on us to meet your needs effectively any or every step of the way
Available for new engagements