Backend engineer, Lagos
Ayomide Ilesanmi
Backend engineer. I design systems that stay up and stay understandable.
Press Ctrl K for the command palettePress ? for shortcutsTab completes, ↑ ↓ history
Career as a trace
Each bar is a span. Select one to see what I built there and with what.
Doxa Spectrum
Mar 2025 to Present
Backend engineering across HR, payroll, billing and business infrastructure within an integrated HR technology platform.
- Payroll engineering
Built payroll computation functionality covering salary calculations, taxes, contributions and deductions.
- Financial services
Developed billing, salary funding and subscription capabilities supporting the wider HR platform.
- Communication infrastructure
Built internal email delivery infrastructure supporting multiple providers, templates and reliable asynchronous delivery.
Architecture explorer
A simplified model of Tour, the travel marketplace I built at Ankur, now in production. Pick an item to book, break a service, and watch what the rest of the system does.
Loading the explorer
Case studies
Short decision records. Context, constraints, the decision, and what it cost.
Tour: a multi-service travel marketplace
Context
Tour is a travel marketplace running in production at thetourafrica.com. Customers book events, stays and cars. Vendors list them and are paid out. It runs as about six independently deployable NestJS services on PostgreSQL: Identity, Catalog, Booking, Payment, Search and Notification. They talk through RabbitMQ events and authenticated service-to-service calls.
I worked on the platform design (service boundaries, API contracts, domain model) and built Identity, Catalog, Booking and Notification. Identity covers accounts, OTP login and token handling, and vendor onboarding with KYC checks. Catalog covers events with tickets, stays with rooms, cars, promotions, places, media uploads, and the dashboards vendors see. Booking owns the reservation lifecycle. Notification runs on RabbitMQ with templates, retries, dead-letter queues and delivery tracking. I also built the search workflow: catalog changes are published over RabbitMQ and indexed in Elasticsearch, so one search spans events, stays and cars. On the money side I fixed the double-entry ledger behind customer virtual accounts, fees, settlements and refunds.
Constraints
- One booking touches several services. There is no shared database, and a customer must never be charged twice or lose a booking because a message was dropped.
- An item must not be sold twice while a customer is paying for it.
- Money has to reconcile. Customer payments, fees, vendor settlements and refunds all have to balance.
- Vendors need identity checks before they can list anything.
- Search covers every kind of listing and has to follow catalog changes closely.
Decision
- Transactional outbox. Identity, Catalog and Booking write each event to an outbox table in the same database transaction as the change. A relay worker publishes it to RabbitMQ afterwards. A crash between the write and the publish cannot lose an event.
- Idempotent consumers. Booking's payment-event consumer deduplicates on a correlation id, retries with exponential backoff, and moves a message to a dead-letter queue when retries run out.
- Short-lived holds. A booking moves from
PENDINGtoHELDtoCONFIRMED. While it is held, the item is reserved in Redis with an expiry, created atomically with a Lua script. If payment never arrives, the hold lapses and the item goes back on sale. - Notification as jobs. Messages are RabbitMQ jobs rendered from templates, with retries, dead-letter queues and delivery tracking. A failed email is recorded and can be replayed, and it never blocks a booking.
- Double-entry ledger. Every money movement is recorded as balanced debit and credit entries, so fees, settlements and refunds can be traced and reconciled.
- Catalog lifecycle. Items move from draft to review to published to archived. Updates use optimistic concurrency, and publishing sends the change to Search.
- Simple tokens. Identity issues signed tokens that other services verify. I moved it from rotating public keys to a single shared signing secret, which simplified key handling.
Trade-offs
- The outbox gives at-least-once delivery, so every consumer has to be idempotent. It also adds a small delay between a change and its event.
- Redis holds are fast and need no row locks, but they are soft. A payment that lands just after a hold expires needs explicit handling.
- A shared signing secret is simpler to run than rotating keys, but every service that verifies tokens has to keep it safe.
- Splitting into services lets each deploy on its own, at the cost of harder debugging across them. That is why services expose metrics, use structured logs and report errors to a team channel.
- Search is eventually consistent with the catalog. A listing can take a moment to appear after it is published.
A bug worth telling
Outbox events stopped reaching RabbitMQ, with no error. The cause was the framework's scheduling layer, not the broker or the database. I replaced the scheduled job with a dedicated relay worker that owns its polling loop and skips a run if the last one is still going. It claims rows with a row lock so several instances can run safely, retries failures with exponential backoff, and after a bounded number of attempts marks the event dead-letter instead of dropping it. It showed up as events missing downstream: services that should have reacted to a change never heard about it.
Result
- Tour is live in production, serving customers and vendors across about six services.
- Events are stored with the data they describe, retried with backoff, and dead-lettered when they cannot be delivered. A failed publish leaves a visible record instead of a silent gap.
- Holds are created atomically and expire on their own, so an abandoned checkout releases the item without anyone cleaning up.
- Vendors go through onboarding with identity checks, and a customer can search events, stays and cars from one place.
- Money moves through a ledger where every entry has a counterpart. The problems that broke that guarantee are fixed: duplicate postings, unbalanced entries and rounding, settlement and refund mismatches, and races between concurrent updates.
Bento: payroll rules for many countries
Context
Bento Africa sells HR and payroll software across African markets. Payroll runs on upp, a shared TypeScript library that the payment microservices import. The gateway's payroll module calls it to price a run for an organisation. It then checks the wallet, takes approvals and triggers payment. The people API holds the HR side: employees, organisations, leave, shifts and onboarding.
My work sat at two points. In the library, I added tax rules for Egypt, Kenya, Ghana, Sierra Leone, Cameroon, Mali, Benin, Burkina Faso, Burundi and Ivory Coast, and pension rules for Burkina Faso, Burundi, Ivory Coast and Mali. In the gateway, I worked on the payroll trigger, the approval emails, and how payroll connects to the people side. I also added billing bands for new countries in the people API.
Constraints
- Every country has its own rules: tax brackets, minimum-wage exemptions, how social security is split between employer and employee, pension, family allowance, and accident contributions. One formula cannot serve them all.
- Money must be exact and carry its currency. Employees in one run can be paid in different currencies.
- Adding a country must not change the result for any other country.
- Changing one employee should not mean recomputing the whole run.
- Money moves only after approval, and only if the wallet can cover it.
Decision
- One pipeline, many countries. A
PayrollBuildertakes each employee through fixed stages, then adds organisation-level billings and fees. A base tax class holds the shared flow: which taxes are enabled, withholding tax, and pro-rating. Each country is a subclass that supplies its own brackets and statutory rules, with a matching pension service. Adding a country means adding files, not editing a formula. - A money type. Amounts are a value and a currency, and arithmetic goes through that type instead of bare numbers.
- Partial recompute.
PayrollDirector.updateEmployeesclones the run's inputs, reprocesses only the changed employees, and merges them into the totals. - Guarded triggers. A payroll records whether it has been triggered and by whom. The gateway debits the wallet per currency and auto-cancels a run that stays unfunded past a grace period.
- Approvals by email. The notification service renders payroll approval schedules from templates, so approvers see what they are signing off.
Trade-offs
- The builder is long, about three thousand lines, which keeps a payroll run in one readable path but is slow to review. The country subclasses limit what a new country can touch.
- The engine is a shared library, so a rule fix reaches a service only when that service upgrades the dependency.
- Rates and brackets live in code. A statutory change is a release, not a settings edit. That keeps results reproducible and makes every change reviewable.
- Test coverage is per country and not yet uniform.
Result
- Tax rules for Egypt, Kenya, Ghana, Sierra Leone, Cameroon, Mali, Benin, Burkina Faso, Burundi and Ivory Coast shipped on the shared engine, with pension rules for four of them. The library now covers tax rules for around thirty countries and pension rules for around twenty.
- The payroll infrastructure supported more than 100,000 employees across the platform.
- Every run records who started it, and approvers receive the payroll schedule by email.
Stack
What I reach for, grouped by the job it does.
Languages
- Node.js / TypeScript
- Go
- Python
- C#
- Java
Frameworks
- NestJS
- Express
- Gin
- .NET
- Spring Boot
Data
- PostgreSQL
- MongoDB
- Redis
- Elasticsearch
Messaging and cloud
- RabbitMQ
- AWS
- GCP
APIs
- REST
- GraphQL
- gRPC
Selected repos
Public work you can read.
ThryifePay GoIn progress
A payment service provider written in Go. Scope: a bank simulator as the upstream, merchant API keys, webhooks, a double-entry ledger, and settlement with payouts.
Double-entry ledger design and idempotent payment flows.
Notes
Short technical write-ups. Drafts are marked.