Client project · API · Oct
Fintech Loan Platform
A loan application that responds in milliseconds but triggers 4 notification channels — SES, Resend, WhatsApp, push — without the API waiting for any of them.
Summary
A serverless loan application platform: form submission, calculation engine, and multi-channel notifications (email via SES, email via Resend as fallback, WhatsApp, push notifications). Built on Go and AWS Lambda, deployed without a persistent server.
The core challenge was the notification fan-out. A loan application submission triggers communications across four channels, each with different latency and failure modes. Making the API wait for all four before responding would make it slow and fragile — a WhatsApp API timeout would time out the entire application submission for the user.
What shipped: 6 Lambda functions (API, loan calculation, SES notifications, Resend notifications, WhatsApp, push), SQS queue decoupling the API from all notification work, sub-100ms API response times regardless of notification provider status.
Architecture Decisions
Why Go on Lambda instead of Node.js
The options considered: Node.js (existing freelance codebase), Python, Go.
The constraint: Lambda cold start time is proportional to binary size and runtime initialisation. Node.js requires loading the V8 engine and all imported modules on cold start. Go compiles to a single static binary with no runtime overhead.
The decision: Go. Binary size ~5MB, cold start under 100ms on a warm Lambda environment.
The trade-off: Go has less Lambda ecosystem tooling than Node.js. Fewer community examples, less documentation for edge cases with the AWS Lambda Go runtime adapter.
What I'd change: Nothing. The performance difference is measurable and the tooling gap is minor once the initial setup is done.
Why SQS decouples notifications from the API
The options considered: Call all notification services synchronously before returning a response, invoke downstream Lambda functions directly from the API, publish to an SQS queue.
The constraint: The API must respond to the user immediately. Notification delivery is a background concern. Providers (SES, WhatsApp) have variable latency and non-zero failure rates.
The decision: The API publishes a single event to SQS and returns 200. SQS triggers each notification Lambda independently. A WhatsApp API failure does not affect the loan submission response — the user's form was saved regardless.
The trade-off: Notification delivery is eventually consistent. The user submits and receives a confirmation before any notification is actually sent. Acceptable for this use case where the submission is the user's primary action.
What I'd change: Add per-channel dead-letter queues with retry policies and CloudWatch alarms from day one. Currently a failed notification silently drops after the visibility timeout expires.
Why 6 separate Lambda functions instead of one
The options considered: One Lambda function containing all logic, separate functions per concern.
The constraint: Each notification channel has different dependencies. SES needs only the AWS SDK. WhatsApp needs a third-party HTTP client. Resend needs its own SDK. Bundling all into one function produces a 3x larger binary, slower cold starts for all channels even when only one is invoked, and a single deployment unit that must be updated when any provider changes their API.
The decision: One function per concern. Each deploys independently, scales independently, and has its own memory and timeout configuration tuned to its workload.
The trade-off: More infrastructure to manage — 6 function definitions, 6 deployment configurations, 6 sets of environment variables.
What I'd change: Nothing. The isolation paid off when the WhatsApp provider changed their API endpoint — only one function needed updating and redeploying, with no risk to the other channels.
Why dual email providers (SES + Resend)
The options considered: SES only, Resend only, active-active with health checking, passive fallback via DLQ retry.
The constraint: SES has sandbox restrictions in early deployment stages that limit which email addresses can receive messages. Resend has better deliverability for certain recipient domains. Neither provider is universally reliable across all use cases.
The decision: SES as primary. If the SES Lambda fails, the message goes to the SQS dead-letter queue, which triggers a retry via the Resend Lambda.
The trade-off: Two providers to maintain, two API keys, two billing accounts to monitor.
What I'd change: Implement active health checking between providers rather than relying on passive DLQ fallback. Currently the switch to Resend only happens after a failure is already occurring.