IDEA Technology · R&D
VendAD
Digital signage CMS for vending machines
- Role
- End-to-end owner
- Links
- GitHub ↗
- Stack
- 50Concurrent devices in the k6 load test
- <1sp95 response time under that load
- <3sFrom approval to every screen, down from ~10 minutes
Overview
A vending-machine advertising platform I owned end to end: the ASP.NET Core MVC portal, the backend and database, a Flutter player for Android TV, and the deployment that keeps a fleet of screens in sync.
What I built
- Owned the whole platform: ASP.NET Core MVC portal, backend, database, Flutter Android TV player and deployment.
- Background workflows with EF Core, PostgreSQL/SQL Server, MediatR, RabbitMQ, Redis and SignalR for media processing, device status and playback reporting.
- Shipped with Docker and GitHub Actions to Render, then load-tested with k6: 50 concurrent devices at p95 under one second.
System design
Event-driven cache invalidation
Approving or editing content publishes a RabbitMQ message. A background worker consumes it and invalidates only the affected Redis keys, so every screen picks up the change in seconds without polling.
Redis distributed lock with Lua
SETNX plus a Lua script serialises concurrent content updates and cache rebuilds across app instances, with a lease TTL so a crashed worker can never deadlock the fleet.
One codebase, two databases
EF Core with provider-agnostic migrations lets the same system run on PostgreSQL or SQL Server by configuration, verified with Testcontainers for both providers in CI.
Video validation pipeline
Uploads pass an ffprobe header check for a fast reject, then a full FFmpeg decode against codec, resolution, bitrate and duration policies before they can reach a screen.
Problems solved
ProblemScreens in different locations kept showing stale content for up to 10 minutes after an admin update.
SolutionContent changes now emit events that a background worker turns into targeted Redis invalidations. Propagation dropped from about 10 minutes to under 3 seconds.
ProblemConcurrent admin edits left the cache inconsistent, so two screens could play different versions of the same campaign.
SolutionCritical writes are serialised with a Lua-based Redis lock that has a lease and auto-expiry, plus a retry queue for failed acquisitions.
ProblemVideos with unsupported codecs reached the players and failed on air.
SolutionA two-stage check: ffprobe for an instant reject, a full FFmpeg decode for safety, and error messages that tell people exactly how to re-encode.
Gallery



Lessons
- Event-driven design decouples publishing from cache invalidation, which keeps the system calm when traffic spikes.
- A naive SETNX lock can deadlock when a worker dies mid-operation. Lua scripts and lease TTLs are not optional.
- Testcontainers for both PostgreSQL and SQL Server removed a whole class of "works on my machine" bugs.