Professional experience · October 2023 – November 2025
PAGOS360
Building the customer platform and improving the backend systems behind payment processing.
I worked across the front-end and backend teams as a full-stack engineer. I led the architecture and initial implementation of the customer platform, and engineered backend synchronisation, queue-based batch processing, tax-recalculation improvements and account-metadata APIs within the core payment environment.
- Next.js 14 · Pages Router
- TypeScript
- React
- Zod
- Auth0
- Jest
- Datadog
- Docker
- DynamoDB
- AWS SQS
- AWS Lambda
- API Gateway
- REST APIs
One full-stack role, two areas of responsibility
PAGOS360 · October 2023 – November 2025
Front-end team
Architecture, delivery and evolution of the customer platform: account views, transfers, API contracts, accessible components, automated checks and production observability.
Explore the front-end workBackend team
CORE and DynamoDB synchronisation, SQS producer–consumer batching, asynchronous IIBB recalculation, account-metadata endpoints and unit and integration testing.
Explore the backend work- Faster selected load paths in internal measurements
- 50–70%
- Jest coverage within the measured front-end scope
- ≈90%
- Expanded product without a complete rewrite
- Beyond MVP
01 / Scope and responsibility
Building the foundation while the product took shape
There was an existing backend domain, a small product group and a fixed MVP date, but no front-end architecture to inherit. I chose most of the core technology, component strategy and development standards, working through important trade-offs with technical leadership and the backend technical lead. Product discussions, existing data structures and regulatory constraints became working requirements.
The first release needed balances, incoming and outgoing movement histories, transfers through domestic account identifiers or aliases, contacts with several destination accounts, and account-specific profiles and roles. PAGOS360 was a regulated payment service provider; the account interface needed to represent that service accurately.
I chose Next.js 14 with the Pages Router for its understood rendering behaviour and compatibility with the identity SDK and React ecosystem. Server-side rendering and API Routes provided the capabilities we needed while limiting the number of new abstractions introduced into a security-sensitive MVP.
02 / Architecture and contracts
One integration boundary, explicit account context
I used the Next.js server as a thin backend for the frontend, or BFF. On the initial request, getServerSideProps validated the session, loaded the user’s profiles and fetched account-specific data. Later browser requests passed through a same-origin API Route, where session recovery, privileged headers, response validation and error translation were centralised.
The browser, the BFF and the application API
A conceptual responsibility map. The BFF centralises integration; the application API remains responsible for business rules and final authorisation.
Diagram preview loads as you scroll. Mermaid source is available below.
View Mermaid source
flowchart TD
accTitle: Front-end architecture and responsibility boundaries
accDescr: React workflows communicate through a Next.js backend for the frontend. This layer manages session context and validates data, while backend services retain business rules and final authorisation.
user[Customer using the banking platform] --> ui[React interface and shared components]
ui --> workflows[Typed forms and explicit workflow states]
workflows --> bff[Next.js backend for the frontend]
auth[Auth0 authentication] --> session[Server-side session and account context]
session --> bff
bff --> contracts[Runtime validation and error translation]
contracts --> backend[Existing backend APIs]
backend --> domain[Business rules and authoritative account data]
backend --> contracts
contracts --> ui
bff -.-> monitoring[Datadog observability]Identity
The authenticated person, established through Auth0.
Profile
The relationship between that person, an account and a role.
Contact
A recipient with one or several destination accounts or aliases.
Keeping those concepts separate gave product, QA and engineering a shared vocabulary. The MVP selected the first available profile, but retained the full profile list in the contract so an account selector could be introduced later without remodelling identity throughout the application.
Evolving API responses needed runtime checks as well as TypeScript types. I introduced Zod schemas at the integration boundary and centralised endpoint calls, parameter serialisation, typed path substitution and transport-to-domain translation. A contract mismatch could then be identified where data entered the application, instead of surfacing as an undefined property deep inside a component.
const accountSchema = z.object({
balance: z.number(),
state: z.string(),
currency: z.object({
codeAlphabetic: z.string(),
symbol: z.string(),
}),
});
type Account = z.infer<typeof accountSchema>;This simplified snippet is reproduced from my article. It captures the historical contract, including its use of number for balances. Moving money to integer minor units or decimal strings is an improvement I identify in the retrospective, rather than a claim about this implementation.
03 / Financial workflows
Making every transfer step explicit
Transfers quickly outgrew a single large form. A user could start from a saved contact or a new identifier, and a contact could have several destinations. Account-holder resolution, recipient confirmation, amount entry and final review each needed a clear place in the flow.
type TransferStep =
| { step: "cbuInput"; cbuAlias: string }
| { step: "contactAddressSelection"; contact: Contact }
| { step: "contactConfirmation"; recipient: Recipient }
| { step: "transferForm"; recipient: Recipient };The discriminated union above is reproduced from the article; Contact and Recipient refer to application domain types omitted from that excerpt. Each step carries the information needed to render it, making the permitted states visible in the type system.
From recipient selection to transfer outcome
A conceptual reconstruction of the implemented journey. Multiple destination accounts require an explicit choice before the user reviews and submits a transfer.
Diagram preview loads as you scroll. Mermaid source is available below.
View Mermaid source
flowchart TD
accTitle: Transfer workflow and recipient selection
accDescr: A saved contact with multiple destinations requires an account selection. A new identifier is resolved before confirmation. Both paths lead to amount entry, review and submission.
start[Start transfer] --> method{Recipient source?}
method -->|Saved contact| count{Destination accounts?}
method -->|New identifier or alias| resolve[Resolve account holder]
count -->|One| confirm[Confirm recipient]
count -->|Several| choose[Select destination account]
choose --> confirm
resolve -->|Resolved| confirm
resolve -->|Not resolved| correct[Correct identifier]
correct --> resolve
confirm --> details[Enter amount and concept]
details --> review[Review sender, recipient and amount]
review --> submit[Submit once and guard navigation]
submit --> outcome{API outcome}
outcome -->|Success| success[Show confirmation]
outcome -->|Failure| failure[Show failure state]I kept a small workflow history so the back action returned to the previous logical step. During submission, the interface guarded navigation and disabled repeat submission, then displayed an explicit success or failure state. These interaction controls complemented the backend: preventing duplicate financial execution ultimately requires server-side idempotency.
04 / Freshness and performance
Useful first renders and predictable refreshes
The server rendered the initial balance, recent movements and contacts. Once the page became interactive, custom hooks refreshed account data at a short fixed interval. Polling paused when the document was hidden, and a failed refresh retained the last successful data. This provided near-real-time updates using the HTTP API available at the time.
Initial server data, followed by controlled polling
The freshness mechanism was polling, with visibility checks and preservation of the last successful result. It did not depend on WebSockets or Server-Sent Events.
Diagram preview loads as you scroll. Mermaid source is available below.
View Mermaid source
sequenceDiagram
accTitle: Server-rendered data followed by visibility-aware polling
accDescr: The first authenticated page receives initial account data from the server. Visible pages poll for updates, hidden pages pause requests and failed refreshes preserve the last successful data.
participant Browser
participant Next as Next.js server
participant API as Application API
Browser->>Next: Initial authenticated page request
Next->>API: Fetch account data in session context
API-->>Next: Balance, movements and contacts
Next-->>Browser: Render useful initial data
loop While the document is visible
Browser->>Next: Periodic refresh request
Next->>API: Request current account data
alt Successful refresh
API-->>Next: Updated data
Next-->>Browser: Apply fresh data
else Refresh fails
Next-->>Browser: Error response
Browser->>Browser: Keep last successful data
end
end
Note over Browser: Pause polling when the document is hiddenFor movement and transfer histories, I moved search, filtering, sorting and pagination to the API. Reusable tables rendered the current query rather than downloading and filtering entire histories in the browser. Filters later moved into the URL, making views refreshable and shareable.
Rapid filter changes could leave several requests in flight. The data layer ignored responses that no longer matched the active query, preventing an older, slower request from overwriting newer results. The reconstruction below illustrates that behaviour through an effect’s lifecycle.
// Reconstructed effect: ignore obsolete read responses.
// queryKey contains the serialised filters, sort order and page.
useEffect(() => {
let current = true;
setLoading(true);
setError(null);
async function load() {
try {
// Illustrative adapter: performs the request and validation.
const result = await movementsApi.list(queryKey);
// A newer query may have replaced this effect while awaiting.
if (current) setRows(result.items);
} catch {
if (current) setError('Unable to refresh movements.');
// Keep the previous successful rows available.
} finally {
if (current) setLoading(false);
}
}
void load();
return () => {
current = false;
};
}, [queryKey]);This example is illustrative, not proprietary source code. The request adapter, query key and surrounding state are omitted. It ignores obsolete results; the original article discusses cancellation as an alternative rather than the mechanism used.
05 / Component ownership
An accessible component system we could maintain
I built an owned component system using shadcn/ui patterns, Radix UI primitives, Tailwind CSS and class-variance utilities. Keeping component source in the application gave us direct control over styling and behaviour, with changes reviewed alongside product code. It also made evaluating upstream fixes part of our maintenance responsibility.
- Navigation and structure. Semantic landmarks, headings and tables, a keyboard-visible skip link and clear current-page states.
- Forms and feedback. Associated labels, descriptions and errors, with
aria-invalid,aria-describedbyand loading announcements. - Keyboard interaction. Operable dialogs, menus, tabs and sorting controls, plus accessible names for icon-only actions.
- Financial formatting. Accessible amount labels that announced currency units and minor units while preserving the expected visual presentation.
The value extended beyond visual consistency: shared components carried interaction patterns into new features. Keyboard and screen-reader testing remained necessary at workflow level because accessible primitives alone could not guarantee an accessible product.
06 / Quality, security and delivery
Making the platform testable and diagnosable
I established strict TypeScript checks, ESLint rules, Prettier formatting and a Jest suite. Each addressed a different concern: type assumptions, unsafe code patterns, review consistency and repeatable behaviour checks. The recorded Jest report reached approximately 90% coverage within its measured front-end scope, alongside manual QA.
I integrated Datadog for browser errors, real-user monitoring, structured logs, resource timing and long-task analysis. Sampled session replay used input masking, environment metadata separated deployments, and correlated server traces helped follow failures through the request path. This improved diagnosis of production behaviour that was difficult to reproduce locally and supplied evidence during audit preparation.
Security work included authenticated page and API boundaries, vulnerability reviews, dependency fixes, filtered forwarded headers and careful treatment of sensitive interactions. Newly created API keys were displayed once and masked afterwards, and selected request and response bodies could be excluded from logging. I worked with backend and infrastructure engineers when findings crossed application boundaries.
I also containerised the application with Docker and a non-root production process. One deployment issue came from public monitoring values being embedded at build time before environment-specific configuration existed. Passing intentionally public values from the server at runtime solved that mismatch, although the custom _app.getInitialProps solution introduced a rendering trade-off.
My compliance contribution was supporting PCI DSS assessment preparation and improving controls within my engineering scope. The experience also reinforced the importance of enforced review, traceable releases and rollback; organisation-wide compliance depends on a broader assessment of technology, people and process.
07 / Backend team · Data consistency
Synchronising CORE and DynamoDB for tax withholding
In the backend team, I worked on the core payment acceptance environment and the IIBB tax-withholding module. My responsibilities included maintaining services, improving algorithms and security, developing APIs, and correcting the way data and calculation states moved between systems.
A central part of this work was a synchronisation layer between the relational CORE database and DynamoDB. The problem was inconsistent information across the two systems. For a calculation that depends on that information, correcting the calculation logic alone is insufficient: its inputs also need to agree.
What I implemented
- A synchronisation layer. I engineered the integration between relational CORE data and DynamoDB to resolve inconsistencies affecting the withholding module.
- Coordination of calculation state. I synchronised tax-retention states across both systems as part of the asynchronous recalculation work.
- Removal of legacy assumptions. I eliminated data drift and hardcoded logic within the affected flows, connecting the correction of stored information with the behaviour of the calculation process.
Data synchronisation and recalculation responsibilities
A conceptual overview of my backend scope: data synchronisation, batch messaging, account metadata and coordinated calculation states. It groups responsibilities rather than reproducing the production deployment.
Diagram preview loads as you scroll. Mermaid source is available below.
View Mermaid source
flowchart TD
accTitle: Backend synchronisation and IIBB calculation consistency
accDescr: Conceptual grouping of the backend contributions described in the resume: CORE and DynamoDB synchronisation, an SQS producer-consumer flow and coordinated asynchronous recalculation. This is not an exact deployment topology.
core[(Relational CORE data)] --> sync[Data synchronisation]
sync --> dynamo[(DynamoDB)]
producer[Producer] --> queue[AWS SQS]
queue --> consumer[Consumer processing batches]
consumer --> coordinated[Coordinated asynchronous updates]
core --> account[Account metadata REST endpoints]
account --> calculation[IIBB tax-withholding calculation]
dynamo --> calculation
coordinated --> calculation
calculation --> result[Consistent data and recalculation states]08 / Backend team · Asynchronous integration
Building an SQS producer–consumer batch flow
I created an AWS SQS producer–consumer algorithm to pass data in batches between decoupled systems. This was backend integration work: one part of the system produced the data to be transferred, while a separate consumer handled the receiving side of the flow.
The engineering concern was both how data moved and how work was organised. Passing batches gave the integration an explicit unit of processing, while the queue separated the producer and consumer responsibilities. That boundary allowed the integration to be understood and maintained as a flow between services.
The batch-messaging boundary
Conceptual view of the SQS flow recorded in my résumé. Batch size, queue type, retry policy and acknowledgement behaviour are not specified by the source material.
Diagram preview loads as you scroll. Mermaid source is available below.
View Mermaid source
flowchart LR
accTitle: SQS producer-consumer batch integration
accDescr: A producer publishes batch work through AWS SQS for a separate consumer to process. This conceptual view omits deployment and delivery-policy details not retained in the resume.
source[Data to transfer] --> producer[Producer organises batch work]
producer --> queue[AWS SQS]
queue --> consumer[Consumer processes queued work]
consumer --> destination[Receiving system]Producer responsibility
Organise the data being passed between systems into batches and publish the work through the SQS integration.
Consumer responsibility
Receive the queued work and process the transferred data on the consuming side of the integration.
09 / Backend team · Calculation correctness
Bringing asynchronous recalculation into agreement
The recalculation work concerned the state of the operation as well as the underlying data. In an asynchronous flow, data updates and calculation steps do not necessarily complete together. Synchronising the tax-retention states between CORE and DynamoDB was therefore part of resolving the inconsistencies.
I optimised these flows by coordinating their states, eliminating data drift and replacing legacy hardcoded logic. The purpose was to make the affected automated calculations operate consistently with the information held across the two systems.
The problem
Inconsistent data, asynchronous tax-retention states and hardcoded behaviour in the legacy recalculation flow.
My contribution
Synchronise the states used by CORE and DynamoDB, remove the identified drift and replace the affected hardcoded logic while preserving existing calculation flows.
- Calculation accuracy reported for the IIBB work
- 100%
- Compatibility reported with existing automated calculation flows
- 100%
These are the results recorded in my résumé for this work. The retained material does not specify the sample size or measurement window; the figures describe the reported calculation and compatibility outcomes, rather than an uptime or system-wide correctness guarantee.
10 / Backend team · APIs and verification
Exposing account metadata without breaking existing flows
I designed and implemented REST endpoints within CORE to expose account metadata. This gave consuming services an API contract for the account information needed by distributed processes, with data integrity and availability as engineering concerns.
My work included unit and integration testing to validate reliability and maintain compatibility with the existing automated calculation flows. That compatibility mattered because a change to the information exposed by CORE could affect processes beyond the endpoint being edited.
- API implementation. Develop the CORE endpoints that exposed the required account metadata to the wider system.
- Behavioural verification. Use unit tests and integration tests to validate changes and their interaction with existing calculation flows.
- Payment-system maintenance. Work within the backend team on core payment acceptance, security improvements and algorithm optimisation using appropriate design patterns.
- Distributed platform context. Contribute within an environment using DynamoDB, AWS Lambda, API Gateway and Docker microservices, with compliance requirements shaping financial-transaction infrastructure work.
The backend technologies above describe the environment recorded in my résumé. My documented implementation ownership is the synchronisation layer, SQS flow, recalculation changes and CORE endpoints; no particular Lambda-to-queue topology or API Gateway configuration is implied.
11 / Outcomes and reflection
The value was a foundation that remained useful
| Contribution | Result and scope |
|---|---|
| Reusable architecture and domain contracts | The MVP grew into a broader product without a complete rewrite. |
| Rendering, bundle and request optimisation | Selected load paths improved by approximately 50–70% in internal measurements. |
| Automated front-end quality controls | Approximately 90% Jest coverage within the measured application scope. |
| Shared components and explicit workflows | Less duplication and clearer account, recipient and transfer behaviour. |
| Production monitoring and trace correlation | Better evidence for diagnosis and audit preparation. |
| CORE-to-DynamoDB synchronisation | Removed inconsistencies affecting the IIBB withholding module. |
| AWS SQS producer–consumer integration | Implemented batch data transfer between decoupled systems. |
| Asynchronous tax-recalculation changes | Eliminated identified data drift and hardcoded logic; the résumé reports 100% calculation accuracy for this work. |
| CORE metadata endpoints and automated tests | Exposed account information and preserved existing calculation flows, with 100% compatibility reported in the résumé. |
What I would change today
I would retain the BFF boundary, runtime validation, explicit transfer steps and owned accessible components. The next iteration would strengthen several areas identified in the article:
- Identity and API boundaries. Purpose-bound access tokens, an allowlisted gateway, explicit request-origin checks and server-managed profile selection.
- Precision and privacy. Minor-unit integers or decimal strings for money, string identifiers and telemetry that excludes sensitive bodies by default.
- Recovery and server state. Separate authentication failures from service failures, consolidate caching and retries, and add back-off and jitter to polling.
- Repeatable delivery evidence. Enforced production controls, generated transport contracts, risk-focused end-to-end tests and continuous route-specific performance baselines.
Those are retrospective improvements. The work I delivered established the architecture, integrations and engineering practices that let the platform grow while making its behaviour easier to understand and investigate.
Based on my résumé and How I Built a Fintech Front End from Scratch and What I’d Change Today. Two highlighted snippets are taken from the published article; the asynchronous example is a labelled reconstruction. All diagrams are conceptual and include reusable Mermaid source.