Skip to content

Writing/Bluetooth

The appliance that was never in the room

I built a coffee machine app without a coffee machine. The constraint turned out to be the most useful decision in the project.

ISIonuț Sandu19 Mar 2026 · 6 min readBluetoothArchitectureReact Native

CoffeeBrew pairs with a coffee maker over Bluetooth, reads how much water and beans it has left, hands it your Wi-Fi so it keeps working when your phone is elsewhere, and brews on a schedule. I built all of it without ever having the machine.

That sounds like a limitation and it was the most useful constraint in the project, because it forced a decision on day one that most connected-hardware apps postpone until it is expensive.

The shortcut that ruins the codebase

The tempting move is to scatter a demo flag through the connect flow. It works immediately, it demos well, and it is unpickable afterwards, because the real behaviour and the fake behaviour end up interleaved inside the same functions. When the hardware finally arrives you are not integrating it, you are excavating.

“Scatter demo flags through the connect flow and the real behaviour and the fake behaviour end up interleaved in the same functions.”

A protocol with six methods

So the device is an interface: scan, read sensors, list networks, provision Wi-Fi, send a brew command. The simulated machine is one implementation of it, with delays, a signal strength that varies, four fake networks and sensor values that drift the way real ones do. The connect flow never knows which implementation it is talking to, because it cannot see past the interface.

protocol.tsTS
export interface CoffeeMakerProtocol {  getScanFilter(): ScanFilter  readSensors(id): Promise<SensorData>  readAvailableWifiNetworks(id)  provisionWifi(id, credentials)  sendBrewCommand(id, command)} // one implementation today, two tomorrow

Swapping in a real machine is a file. That is the whole claim, and it is checkable: there is no branch anywhere in the app that asks whether the device is real.

The part that is real regardless

Everything above that interface is built properly, because none of it depends on the hardware existing: the pairing flow with all its failure states, the backend, the checkout for beans, and the scheduled brews.

That last one is worth its own note. A scheduled brew is worthless if the reminder needs the app to be open, and a client-side timer is exactly that, because it dies with the process and the operating system will kill the process. So the schedule lives in the database and a cron job inside the database sends the notification. The phone is a client of the schedule and never its owner, which is true whether or not there is a machine at the other end.

Ionuț Sandu — 19 Mar 2026Copy link

NEXT · 22 AUG 2026 · 8 MIN READ

The cheap design that measurement ruled out

A pollable endpoint looked like it would do the job. Seventy-eight minutes of measuring two real accounts showed it was wrong by up to forty-three minutes, and that decided the architecture.

Taking on new projects

Tell me what you’re building.A written scope and a fixed quote before any money moves.

I reply to everything within one working day. If I’m not the right person for it, I’ll tell you straight away rather than three weeks in.