When I’m not building software for a living, I’m usually trying to make my house do something it wasn’t designed to do. The latest target wasn’t a light or a thermostat. It was my car.
I wanted my Volvo XC90, now nine years old, to show up in the Apple Home app, right between the living room lamp and the front door sensor. Lock it from my phone. Glance at the remaining range from the kitchen instead of walking out to the driveway. Get a nudge if I walked away and left a window down. By the end I had all of that and more running through a Homebridge plugin.
Jargon Lookup: Homebridge A lightweight Node.js server that emulates the Apple HomeKit API. It acts as a universal translator, taking signals from devices Apple doesn’t natively support and wrapping them in a data structure that HomeKit understands.
But the features aren’t the interesting part. The interesting part is that almost every problem I hit on this hobby build was the same class of problem I’ve spent years solving in enterprise systems. A car-in-a-phone-app turned out to be a surprisingly faithful model of integration engineering. Here’s what it taught me.
1. The Domain Mismatch — HomeKit Doesn’t Have a “Car”
The first wall you hit is philosophical. Apple’s HomeKit has a fixed vocabulary of accessory types: lights, locks, thermostats, contact sensors, a handful of others. There is no “vehicle.” There is no “fuel gauge,” no “odometer,” no “tyre pressure.”
A car produces dozens of rich data points. HomeKit offers you maybe thirty rigid primitives, every one of them designed for a bungalow. So you don’t get to model your domain. You get to translate it — and translation always loses something.
Repurposing a primitive means taking a HomeKit service built for one purpose and bending it to display something else, accepting that the label will be wrong but the information will be right. So fuel range became a Light Sensor — the only primitive with a numeric range big enough to hold “550 km.” The Home app cheerfully displays it as “550 lux.” Low brake fluid became a Leak Sensor, because a water-drop icon and a “Leak Detected” alert is the most honest available metaphor for a fluid is low. “Is the car parked at home?” became an Occupancy Sensor, with a bit of trigonometry converting GPS coordinates into occupied/not-occupied.
I also learned where not to force it. I tried modelling “service due” as a Filter Maintenance service — it even has a percentage and a “Change Filter” alert, conceptually perfect. The Home app rendered it as a grey box reading “Not Supported.” Some primitives only work when chained to a parent accessory that doesn’t exist in this context. I reverted to a plain contact sensor and moved on.
The enterprise parallel: this is an anti-corruption layer. When you integrate a sophisticated source system into a target with a poorer or differently-shaped schema, you don’t get to invent new types in the target. You map honestly onto what exists, you pick the least-misleading representation, and you know when to stop forcing a metaphor the platform will reject.
Jargon Lookup: Anti-corruption layer A translation boundary between two systems that model the world differently, so the messiness of one schema doesn’t leak into and corrupt the other. The craft lives in the mapping, not the data.
2. The Rate Limit — Designing Around 10,000 Calls a Day
Volvo’s developer API currently allows 10,000 requests per day per app. That sounds generous until you do the arithmetic.
My plugin exposes a dozen tiles. The naive design has each tile fetch its own data on every poll. At a 150-second poll interval — fast enough to feel responsive — that’s 576 cycles a day. If each of ten tiles fires its own request every cycle, you’re well past the ceiling before lunch. Worse, several tiles need the same underlying data: the lock state and the door sensors both come from one endpoint; two different range tiles both need the same statistics call.
Jargon Lookup: Cache stampede Also called a thundering herd — when many consumers independently request the same expensive resource at the same moment, multiplying load for data that could have been fetched once.
The fix was a small in-cycle cache with three layers. First, in-flight request coalescing: if two tiles ask for /windows within the same poll, the second one joins the first one’s pending promise instead of firing a second call. Second, an adaptive TTL — time-to-live, how long a cached value stays valid — scaled to the poll interval, long enough to collapse a cycle’s worth of duplicate reads but mathematically guaranteed to expire before the next cycle, so data is never stale. Third, a generation counter so that a manual “refresh now” button always bypasses the cache and can never accidentally serve a value that was fetched before you pressed it.
That took the system from twelve calls a cycle to ten, and — more importantly — made the daily budget predictable. I can now tell a user exactly what interval is safe.
The enterprise parallel: this is request coalescing and read-through caching, the bread and butter of any API gateway sitting in front of a rate-limited or expensive downstream. The lesson that doesn’t fit on a slide is that correctness lives in the edge cases — the manual refresh racing a scheduled poll, the request that started before a cache invalidation and resolves after it. “It works once” and “it works 576 times a day without ever serving a stale byte” are different engineering problems.
3. Optimistic State — The Switch That Flipped Itself Back
Here’s a bug that nearly broke my brain. I’d tap the “Climate” switch in the Home app. It would turn on for half a second, then snap defiantly back to off — even though the command had been sent and the car was heating up.
The cause is a timing assumption baked into HomeKit. When you flip a switch, the app sends a SET, then immediately sends a GET to confirm the new state. My GET handler was honestly reporting the car’s state — which, one second after the command, the slow remote API still reported as “off.” HomeKit saw “off,” concluded my SET had failed, and reverted the toggle.
Jargon Lookup: Read-after-write consistency The guarantee that once you’ve written a value, a subsequent read reflects it. Distributed systems frequently don’t offer it, and UIs that assume it will misbehave.
The fix is optimistic state: the instant the user flips the switch, I update my local notion of the state and serve that to the verify-GET, before the slow API call has even returned. If the command later fails, I revert and tell the app. The switch now behaves, because the read reflects the user’s intent immediately rather than the backend’s lagging truth.
The enterprise parallel: this is the optimistic-UI half of CQRS, and the eternal tension between the command path and the query path. Anywhere your write goes to a system that’s slow to make that write visible to reads, a naive “just go ask the source” will produce flicker, phantom failures, and confused users. You design the read model to honour intent.
Jargon Lookup: CQRS Command Query Responsibility Segregation — separating the path that changes state (commands) from the path that reads it (queries), so each can be modelled and optimised independently instead of fighting over one shared view.
4. Eventual Consistency — The Car That Lied About Its Own Doors
My favourite lesson came disguised as a bug report I filed against myself. I’d unplugged the charging cable and gone for a drive, but the “Charger Connected” sensor stubbornly stayed connected for twenty minutes. I was convinced the plugin was broken.
It wasn’t. The car’s cellular modem only phones home when the vehicle is awake. Park it, and the modem eventually goes dormant; the API keeps serving the last state it was told. When I checked the raw response, every field carried a timestamp two hours old. The API wasn’t lying — it was faithfully reporting stale truth. I was the one assuming real-time.
Jargon Lookup: Eventual consistency The system will reflect reality eventually, but at any given instant a read may be out of date. You build for it by designing around staleness rather than pretending it away.
This reframes what you’re even allowed to promise. I built a “you left something open” notification — lock the car with a window down and it alerts you. It’s genuinely useful. But it is best-effort, not a safety guarantee, because the source it depends on is eventually consistent. The honest move was to design for the staleness, surface the data’s age in the logs, and never market the feature as something it physically cannot be.
The enterprise parallel: this is every distributed system, every replicated database, every event-driven architecture with a downstream that lags. The mature engineering response is the same in the garage and in the data centre: you cannot offer a stronger consistency guarantee than your weakest source. You expose freshness, you degrade gracefully, and you resist the product pressure to promise real-time on top of something that isn’t.
Architecting the Honest Mapping
Looking back, the features were the easy part. Locking a car over an API is a few lines. The real work — the part that took the iterations — was everything around the features: translating a rich domain onto a poorer vocabulary without lying to the user, staying inside a hard rate limit without going stale, making an optimistic UI feel solid on top of a sluggish backend, and being honest about what an eventually-consistent source can and can’t guarantee.
Those aren’t car problems. They’re the constraints I’ve spent fourteen years on in enterprise systems — wiring a legacy ERP into a modern app, putting something dependable in front of a flaky third-party API, making a distributed system feel instant without lying about its consistency. A nine-year-old Volvo just happened to hand me all four in one weekend project.
It’s also the kind of work Codzelerate exists to do — integrations, IoT, and Apple-platform engineering where the interesting bit is always the constraints, not the happy path. If you’ve got a system that needs to talk honestly to another system, get in touch. I genuinely like talking about this stuff.
The plugin is open source: homebridge-volvo-xc90 on GitHub, published on npm.
Stay Ahead of the Curve
New posts weekly — on system integration, low-code architecture, and the parts of enterprise engineering that don’t show up in marketing copy. Subscribe to get them in your inbox.
Further Reading
- What Home Automation Taught Me About Enterprise Orchestration: The companion piece — why managing a fragile smart-home stack made me a better enterprise architect, and the decoupling patterns that show up in both.
- Breaking the Low-Code Monolith: Guide to Event-Driven Design: The deeper dive into why polling suffocates your infrastructure and what listening for events instead actually buys you.
- Designing Enterprise APIs in Appian: Beyond the Basics: How to build the translation layers between systems that were never meant to talk to each other — the anti-corruption layer, at enterprise scale.