Three pieces of work detailed end to end: context, constraints, diagnosis, decisions, measured results, and what I took away. The point is not to display a stack, but to show a line of reasoning.
Database optimization — AWS RDS / MariaDB Link to heading
Context Link to heading
Multi-channel media platform (65 radio stations, 460,000 monthly sessions) running on a MariaDB database hosted on AWS RDS (db.m7i.xlarge — 4 vCPU, 16 GB), used by every service: public site, editorial back-office and custom API (Drupal), mobile app. The Drupal-imposed schema is non-trivial and far from optimal — a constraint of the foundation, not a team choice. April 2026, three weeks total.
Stakes Link to heading
150 journalists depend on the back-office: every saturation blocks editorial production. The public site and the mobile app rely on the same database — the slowdowns hit listeners too. Load was rising over time: without action, saturation would become the norm.
Constraints Link to heading
No service interruption acceptable. A 3-developer team: no one could be assigned full time. Scaling the instance up was ruled out from the start — too expensive, and the instance was already oversized relative to its nominal load. Moving up a tier would have treated the symptom, not the cause.
Diagnosis Link to heading
Symptoms: 3–4 load spikes a week, unpredictable, reaching 400 AAS (Average Active Sessions) on 4 vCPU — roughly 100× the concurrent processing capacity; 100% CPU, 30 to 90 minutes per episode; 500 and 504 errors.
Two distinct load sources: the Drupal cache rebuild (triggered by deployments and a few rare editorial actions), instantly correlated with the spikes; and some costly queries, independent of the rebuild, identified in the Top SQL.
Investigation Link to heading
Tools: RDS Performance Insights (Top SQL ranking) and CloudWatch (CPU, AAS). Approach: correlate triggering events with observed spikes, collect the costliest queries during and outside spikes, analyze data-access patterns.
The investigation was accelerated by an LLM (Claude) under a strict frame: inputs provided (relevant code, Top SQL, system context); imposed workflow (analysis → hypotheses → critique of options → human validation → report → prioritized batches → execution tracked by status). The LLM sped up analysis and structured execution; solution validation and trade-offs stayed human.
Decisions Link to heading
Targeted indexes on the attributes the analysis pointed to rather than placed at random; rewriting the costliest queries; fixing data-access patterns. Work carried out in prioritized batches (importance × compatibility), with before/after measurement at each batch, no service interruption.
Results Link to heading
Measured one month before / one month after:
- RDS CPU load down 54%;
- main saturation source gone — no more 100% CPU spikes;
- residual spikes at 50 AAS (vs 400), far less frequent;
- sharp drop in 500/504 errors on the back-office.
The lower load now makes a cost-reducing instance downsizing possible — sizing analysis in progress. The cost reduction is not realized yet.
Takeaways Link to heading
Significant gains rarely come from micro-optimizations: they come from understanding the system as a whole. The “scale up” reflex would have cost a lot and solved nothing — understand before you spend. A CMS-imposed schema does not preclude major gains, provided the analysis points precisely at where to act.
Integration gateway between providers — AWS serverless Link to heading
Context Link to heading
The platform relies on two external providers: a donation platform (online collection) and a CRM (donor management). No native integration, and the two providers could not make their systems talk to each other. Designed and built alone, November — December 2025.
Stakes Link to heading
Donations represent over 50% of the station’s funding. Every donation had to reach the CRM; without an integration, manual entry or lost data. Reliability non-negotiable: a lost donation is unacceptable, a double-counted one too. Spiky load: 2–3 campaigns a year, one year-end campaign alone accounting for ~50% of the take.
Constraints Link to heading
No control over either system: input and output formats imposed. Imminent campaign. Running cost as low as possible.
Architecture choice Link to heading
The donation platform emits a webhook on each donation: no polling needed, the architecture can be fully event-driven. Serverless on AWS for three reasons:
- stack already mastered — the platform’s infrastructure is on AWS, no new ecosystem to operate;
- cost aligned with usage — load arrives in spikes; a permanent server would be paid to do nothing most of the year;
- managed building blocks — secrets, queues, retry provided by the platform rather than hand-coded and maintained.
Decisions Link to heading
Serverless gateway described as infrastructure as code (CDK):
| Block | Role |
|---|---|
| API Gateway | webhook entry point |
| Lambda | transform format A into format B |
| DynamoDB | uniqueness — deduplication by transaction ID |
| SQS | decoupling and retry: 3 attempts before dead-letter |
| Secrets Manager | both providers’ tokens |
| IAM | dedicated, minimal permissions |
| Dead-letter queue | no-retry queue → e-mail alert → manual handling |
Two-level error handling: transient → automatic retry (SQS, 3 attempts); structural (e.g. an attribute rejected by the CRM API, where retrying changes nothing) → dead-letter, e-mail alert, manual handling by support.
Implementation Link to heading
Built in about two months, alone, alongside ongoing work. Progressive rollout: batched sending to test load and validate behavior, then switch to continuous flow.
Results Link to heading
- in production since early 2026;
- ~10,000 donations processed, zero incidents;
- zero duplicates — uniqueness guaranteed by design (transaction ID in DynamoDB);
- no donation lost — failures go to the dead-letter queue and are handled manually;
- running cost: €4 per month, for a flow carrying over 50% of funding.
Takeaways Link to heading
Designing for failure (queues, retry, dead-letter, alerts) costs little upfront and turns errors into support tickets rather than production incidents. Between two systems you don’t control, you own only two things: the data contract and idempotency — the transaction ID as uniqueness key makes duplicates impossible by design. Serverless fits when load arrives in spikes.
Data migration during the merger (2025) Link to heading
Context Link to heading
In 2025, the radio union merged with another radio that had its own platform (site, back-office, editorial data, media). Goal: serve all stations from a single platform — ours — and decommission the incoming radio’s. Project led and carried out alone, as a background thread over 6 months, with a management-set deadline.
Stakes Link to heading
Editorial continuity: the incoming radio had to keep publishing during the migration. Around ten journalists to onboard into the back-office. Several hundred GB of media to bring over.
Constraints Link to heading
No usable export on the source side: data was only accessible through their platform’s API. Incompatible data models. Media to bring up to internal standards. Project run alone, without pausing ongoing work.
Diagnosis Link to heading
Scope decided with management and the newsroom: migrate the last two years of history (4,000 episodes, 1,500 articles, with their media) — not all history had the same editorial value, and each extra year would have lengthened the migration with no proportional benefit. Chain broken into four steps: extract (source API) → transform (A → B mapping) → bring over media → load.
Decisions Link to heading
- Extraction: full traversal of the source API in Python, with pagination and retry on network errors.
- Transformation: Python scripts producing a JSON pivot format; scrupulous mapping (dates, categories, links to existing objects). Attaching content to the “radios” taxonomy which, through the back-office access control, automatically grants incoming journalists the right permissions — no manual permission management.
- Media: several hundred GB (audio, images) brought over and stored on S3, brought up to internal standards (dimensions, renaming).
- Loading: import through the existing Drupal Feeds module rather than a custom importer — less code to write and maintain, and entry through the CMS’s official path (validations and hooks included).
- Strategy: batch migration (import → test → validation → next batch), validated by me and the business teams on a simple source/target similarity criterion; idempotent replay — a batch can be re-run safely.
Results Link to heading
- two years of history migrated: 4,000 episodes, 1,500 articles, with their media (hundreds of GB);
- no data loss;
- ahead of deadline;
- source platform decommissioned — infrastructure savings as a result;
- around ten journalists onboarded; the platform now serves 65 radio stations.
Takeaways Link to heading
Batch migration with business validation at each step turns a risky project into a series of controlled steps — trust is built batch by batch. The hard part is not the volume: it’s the mapping between two data models thought out differently. Idempotency makes error recovery trivial — same principle as the donation gateway: design for replay rather than for the happy path.