Software Engineer
Owned a payment flow end to end
Architecture discussions, backend design, implementation, testing, deployment and production support for the MHub integration, as the primary developer on it. Payments that used to be keyed in by hand through a UI now flow through automatically.
Non-blocking enrichment from CRD
Settlement instructions arrive asynchronously over Kafka, so payments wait in a correlation cache keyed by instruction id while the pipeline keeps processing everything else.
Legacy and ISO 20022, both directions
Outbound, MT-era payment data becomes a pacs.008. Inbound, pacs.002 and pacs.004 responses become the CDMMT representation the upstream system can actually read.
One image, several runtimes
The same ECR artifact deployed as separate ECS services, each resolving its Kafka topics, source systems and CRD behaviour from DynamoDB configuration at startup.
Alerting for silence, not just errors
Lambda-backed alerts for a payment nobody answered: no Digital Core response in ten minutes, no SWIFT response in thirty, routed to the team that owns the failure.
Lifecycle you can actually query
Every meaningful transition persisted to DynamoDB and propagated back through GMTC, so a payment's position in a distributed flow is a fact rather than a guess.
I worked as a software engineer on a mission-critical payment platform supporting capital markets payment flows across several upstream trading systems, internal services, and downstream settlement networks, processing thousands of payments worth over $100 million a day. The work was backend engineering: distributed systems, payment orchestration, ISO 20022 modernization, cloud infrastructure, and production operations, in Java with Spring Boot, WebFlux and Project Reactor, over Kafka, DynamoDB, Lambda, ECS, ECR, CloudWatch, Secrets Manager and Cognito.
The most significant project I owned was a new payment-processing flow for Mortgage Hub, or MHub. I was the primary developer: I designed the backend implementation and built the service from the initial architecture discussions through implementation, testing, deployment and operational support. Before it, those payments were entered by hand through a UI; the new flow releases them automatically.
Owning it from architecture to production
My involvement began before implementation. I took part in the requirements and solution architecture discussions where the team decided how the new flow would fit the existing payments ecosystem, which gave me visibility into the system before there was a plan or a line of code to work from.
Once the architecture was settled I designed the backend implementation myself: breaking the solution into processing stages, defining how messages would move through the service, deciding how asynchronous dependencies would be handled, and planning the validation, transformation, persistence and status-management logic.
I reviewed the design with my manager as it developed. Those reviews were about engineering quality rather than someone handing me the solution. One recommendation I took was to introduce domain-specific exceptions instead of leaning on generic ones, so different failure conditions could be represented and handled distinctly. After the design was approved, I implemented it.
MHub, front to back
The integration was built as a reactive, event-driven flow inside the Payment Preparation Service. PPS sat between several upstream trading platforms and the downstream payment infrastructure, and its responsibility was not simply to convert one message format into another. It was an orchestration layer: receive payments, enrich them with external data, validate them, construct payment messages, maintain lifecycle state, send payments downstream, and propagate acknowledgements or failures back to the systems they came from.
MHub was one of several upstream systems, and rather than PPS exposing a different integration contract to each of them, messages first passed through GMTC, an internal normalization service. For this flow the source payload was XML carrying an embedded SWIFT MT message, a CRD settlement-instruction identifier, and the additional payment fields needed downstream. GMTC wrapped that raw payload in a standardized JSON envelope, identified MHub as the originating system, and published it to the Kafka topic the relevant PPS runtime consumed. The envelope let PPS know which source it was dealing with while keeping the original payload for source-specific processing.
A reactive pipeline rather than a request per thread
Inside PPS a Kafka listener consumed payment records and emitted them into a Reactor sink, exposed as a Flux so the application processed a continuous stream of payment events through Project Reactor and WebFlux. Each incoming payment became an internal model and passed through a sequence of processing stages: for MHub, parsing the source XML and pulling what was needed out of the payload, the embedded MT message and the CRD identifier.
The payment could not become its final ISO 20022 message until PPS had the client's settlement instructions, which is where the interesting distributed-systems problem started.
Enriching from CRD without blocking
Settlement instructions live in CRD, the internal client reference data system, and MHub gave us the identifier needed to retrieve them. The interaction was asynchronous and Kafka-based: PPS published a request carrying the identifier, CRD consumed it, retrieved the instructions, and published a response some time later. There was no synchronous thread waiting.
So I implemented a correlation mechanism. When PPS sent a request, the payment waiting on that settlement instruction went into an in-memory cache keyed by the identifier, which also handled the case of several concurrent payments needing the same one: the key mapped to every payment waiting on it. While they waited, the application carried on processing other events instead of holding resources open.
When the response arrived, PPS pulled the identifier out of it, looked up whatever was waiting under that key, removed those entries, enriched the payments with the returned instructions, and pushed them back into the pipeline as a Flux so processing resumed. Architecturally it is asynchronous request-response correlation inside a reactive service: business processing for one payment suspends without blocking the thread that is handling everyone else's.
Building the pacs.008
Once a payment had its settlement instructions, PPS had everything required to construct the final message. It combined the MHub XML fields, the embedded MT message, the CRD instructions and internal payment metadata, and mapped them into an ISO 20022 pacs.008 customer credit transfer, generated through Java SWIFT libraries and conforming to what the Canadian payment ecosystem expects. The completed MX message was wrapped in the JSON contract the downstream platform expected and sent to Digital Core over Kafka.
This stage was more than moving fields between objects. Information originating in a legacy trading platform had to be represented correctly inside a modern ISO 20022 structure while still satisfying downstream validation and settlement requirements.
Lifecycle as durable state
Payments crossed several independent systems, so it mattered not only whether one eventually succeeded or failed but where it currently was. I implemented lifecycle transitions recording the meaningful events in DynamoDB: payment received, CRD request sent, CRD response received, validation completed, MX generated, sent downstream, Digital Core acknowledgement, downstream validation, SWIFT or Lynx acknowledgement, rejected.
Persisting those states gave the service a durable representation of each payment's progress, and I worked with the DynamoDB data models and global secondary indexes that made lookups and concurrent lifecycle processing efficient. In practice PPS provided something close to a state machine over a distributed, asynchronous flow.
The path back
Internal state was only half the requirement: the originating system needed to know what was happening to its payment. Whenever a meaningful lifecycle change occurred, PPS sent a status update upstream to GMTC, which exposed the payment's current state through its UI for support and business users and propagated the update further up to MHub. Payments travelled one way and operational feedback travelled the other.
Processing did not end when the payment reached Digital Core. It could acknowledge receipt, downstream validation and routing components could produce their own acknowledgements, and eventually SWIFT or Lynx could acknowledge or reject. Any of them could reject instead. In either case PPS identified the original payment, determined the new lifecycle state, persisted it, generated the appropriate upstream status, and propagated it through GMTC to the originating system, which is what gave the platform end-to-end traceability instead of a black box after handoff.
MHub was a legacy MT-based system and could not consume ISO 20022 responses, but downstream answers arrived as MX: pacs.002 status reports and pacs.004 returns. So the integration needed transformation in both directions. Outbound, MHub information became an ISO 20022 payment; inbound, I implemented the logic that turned those MX responses into the CDMMT representation the upstream system understood before sending them through GMTC. PPS was, in effect, a compatibility layer between two generations of financial messaging, which is exactly what the industry's transition to ISO 20022 requires of the systems in the middle.
One image, several runtimes
PPS was not one large application. It was deployed as several independently configured ECS services, all running the same container image from ECR, each one consuming the topics and handling the source systems its configuration told it to.
Configuration decides behaviour
Each service received a runtime identifier through its ECS configuration. At startup the service used that identifier to resolve the DynamoDB configuration records belonging to it, covering which inbound Kafka topic it consumed, which topics it published to, which source systems it accepted, whether CRD integration was required and how it was configured, and other flow-specific behaviour. Those records were loaded into an in-memory entity cache and referenced during processing, so the service was not querying the configuration store for every payment.
The result is a configuration-driven deployment model: source system, plus ECS runtime, plus DynamoDB configuration, determines processing behaviour. One application implementation supports several independently deployable payment streams. A problem or a deployment on one runtime does not require stopping the others, each stream scales at the ECS-service level, every instance is built from the same artifact so there is no drift between them, and a flow can be started, stopped or reconfigured on its own. In practice that last part was literal: starting and stopping a stream during deployments and integration testing meant moving that ECS service's desired task count between one and zero.
The instances split by flow. One handled the primary ISO flows: WSS, Murex, Calypso and Falcon. Another handled MHub and Calypso PSH. ICI had its own. Falcon PSH had its own as well, and it is the one that differs most.
The difference is the downstream leg. Digital Core is shared, but each runtime reaches it on its own connection rather than through anything pooled: three of them publish to it over their own configured Kafka integration, and Falcon PSH does not publish at all. It calls the Digital Core API over REST, which is why it is the only instance that needs a token.
Falcon PSH: REST, and a token worth keeping
Falcon PSH sent payments to Digital Core through a REST API, and during the project Digital Core's authentication requirements changed: outbound requests now had to carry a valid Amazon Cognito token. I implemented that authentication.
The service pulled its authentication configuration from AWS Secrets Manager, keeping credentials out of the source and the config files, and used it to request a token from Cognito. Requesting a fresh token per payment would have added an authentication round trip to every message, so tokens were cached in memory with the cache TTL aligned to the token's own expiry. Subsequent payments reused the cached token; once it expired the entry disappeared, the next request found nothing valid, and the service acquired and cached a new one before attaching it to the outbound call. Simple token lifecycle management, and it kept the authentication traffic proportional to token lifetime rather than payment volume.
I also worked on the Calypso PSH flow, where PPS did not perform the outbound MT to MX transformation, the downstream system did. The response path still needed work: when downstream processing returned a pacs.002 or pacs.004, the upstream Calypso system expected a legacy-compatible response, so I implemented the transformation and propagation back through GMTC. Another instance of PPS sitting between two generations of messaging.
Validation that has to answer upstream
For the payment streams whose architecture was already established, Calypso, Falcon, WSS, Murex and other existing ISO flows, my work focused on keeping PPS compliant with updated SWIFT and ISO 20022 requirements. I implemented new validations required by updated standards and business rules, applied at different points in the lifecycle depending on the message type and originating system.
The important part is that a failed validation could not simply raise an internal exception. Upstream systems needed to know the payment had failed and why, so PPS constructed the appropriate rejection according to the originating system's contract and propagated it back. That put validation inside the lifecycle and response architecture rather than off to the side as input checking.
Alerting for the failures nobody is watching
Because payments crossed several independent systems, PPS could hand one off successfully and never receive the response it expected. Those conditions had to surface on their own rather than waiting for someone to notice a stuck payment, so I built operational email alerting for scenarios including:
- a payment being rejected
- no response from Digital Core within ten minutes
- no response from SWIFT within thirty minutes
- validation failures inside PPS
- invalid CRD responses
- missing or insufficient information in an upstream MHub message
The alerts were context-aware rather than a single distribution list. A CRD response failing our validations could be routed straight to the CRD team, so the people who owned the source data were the ones who heard about it. I implemented the workflows in AWS Lambda, which kept email delivery off the critical payment path, and together with the DynamoDB lifecycle records and CloudWatch they gave the platform observability into the failures that actually need a human.
Failure paths as first-class design
Payment systems make you treat failure as part of the architecture rather than an afterthought. A payment could fail because upstream information was missing, because the payload could not satisfy validation, because CRD returned invalid instructions, because MX construction failed validation, because Digital Core or an intermediate system or SWIFT rejected it, or because an expected response simply never arrived inside its operational threshold.
Each of those needed its own combination of exception handling, lifecycle state update, persistence, upstream rejection, notification, logging and operational visibility. It changed how I write backend code: the question stopped being whether the happy path works and became what the system should do when any dependency, message, validation or downstream component behaves unexpectedly.
Testing across seven flows
I supported end-to-end testing across roughly seven payment flows, including ones I had not implemented. ICI was one of them, a system I had worked with as a business analyst, where I supported the end-to-end validation rather than the development.
Testing a distributed payment platform means validating far more than individual methods, so I followed payments across system boundaries: source-system ingestion, GMTC normalization, Kafka delivery, PPS processing, CRD enrichment where required, transformation, downstream delivery, acknowledgements, rejection handling, DynamoDB lifecycle updates, upstream status propagation, and the final SWIFT or Lynx response. It gave me a much broader picture of the platform than the parts I had written myself.
Deployment, containers and the command line
The applications were containerized with Docker and deployed to Amazon ECS. I configured and supported the GitHub Actions pipelines that built images, pushed them to ECR and deployed across sandbox, SIT and production, and I worked with ECS service configuration and task execution rather than treating deployment as somebody else's stage.
I used CloudWatch heavily when troubleshooting, reviewing service and task logs and tracing failures through the processing stages to work out whether the cause was application logic, malformed payment data, Kafka communication, a downstream dependency, deployment configuration, authentication or the infrastructure itself. In a distributed system a visible failure in one service is often not caused by that service, so debugging usually meant following a payment across components.
Working inside a bank also meant much of this could not happen from a local machine. Access to certain environments and resources went through secured servers, so I spent a lot of time in Linux command-line environments with SSH, shell scripts and the AWS CLI, including database and record-management tasks that security requirements kept out of local tooling. It made me considerably more comfortable operating real infrastructure.
Technologies and concepts
Backend
- Java
- Spring Boot
- Spring WebFlux
- Project Reactor
- Flux and Mono
- reactive programming
- domain-specific exception handling
Messaging and distributed systems
- Apache Kafka
- event-driven architecture
- asynchronous request-response
- event correlation
- reactive stream processing
- distributed state management
- bidirectional status propagation
Payments
- SWIFT
- ISO 20022
- MT and MX messaging
- pacs.008, pacs.002, pacs.004
- CDMMT
- settlement instructions
- payment lifecycle management
- rejection handling
- legacy to ISO interoperability
AWS and infrastructure
- Amazon ECS and ECR
- AWS Lambda
- DynamoDB and GSIs
- AWS Secrets Manager
- Amazon Cognito
- Amazon CloudWatch
- AWS CLI
Delivery
- Docker
- GitHub Actions
- CI/CD across sandbox, SIT and production
- container lifecycle management
- operational monitoring
- REST integration and token caching
What I took away from the role
The biggest shift came from seeing backend code become one component inside a much larger production ecosystem. A payment-processing service cannot be designed in isolation: every decision has consequences for upstream trading platforms, downstream settlement systems, persistence, monitoring, operational teams, and ultimately for whether anyone can say what happened to a financial transaction.
A small change could affect several systems. A validation rule needed a rejection path. An asynchronous request needed a correlation mechanism. A downstream dependency needed timeout monitoring. A new authentication requirement needed secure credential storage and token lifecycle management. A message transformation needed to preserve the meaning of financial data across two standards. And a successful deployment still needed monitoring, lifecycle controls and support behind it.
By the end I had worked across almost every stage of a backend service's life: solution architecture, implementation design, reactive pipelines, external integration, financial message transformation, state management, failure handling, observability, container deployment, cloud troubleshooting and end-to-end testing. It gave me a much stronger sense of what it means to own a backend system rather than contribute code to one, and it confirmed that large-scale backend and distributed systems, where reliability, performance, correctness and architecture genuinely matter, are the part of this field I want to be in.