Back to Blog
technicalarchitectureconnectors

"What the Graph Owes: Connectors That Drive Outputs"

May 18, 2026AimDB Team7 min read

Most connectors carry data. A sensor publishes a reading. A dashboard receives it. If the reading arrives a second late, the chart is a second late. Telemetry is patient.

Some connectors carry consequence.

A link_from on a SingleLatest buffer isn't observing the world. It's reaching into it. The pipeline that follows that buffer doesn't have the luxury of being a passive consumer, it has to act. A setpoint becomes heat. A command becomes motion. An off becomes off.

Which raises the second question the connectors post put on the table and that the previous post deferred: when a connector drives an output, what does the rest of the graph owe it?

Three Things the Graph Owes a Consequential Consumer

When the next tap after link_from is going to do something irreversible, the graph owes it three things:

  1. Freshness — the instruction it's about to apply was not already obsolete when it arrived.
  2. Ordering — newer instructions supersede older ones. Never the other way around.
  3. At-most-once-applied — the same instruction does not act on the world twice.

These aren't abstract. Skip freshness and you act on a stale setpoint after a reconnect. Skip ordering and an old "off" arrives after a new "on". Skip at-most-once and a duplicate retransmit fires the same actuator twice.

These are transport-boundary concerns and in AimDB they should live in the connector crate rather than the runtime core. The runtime provides buffer policy and graph composition, the connector defines boundary behavior such as delivery semantics, retries, ordering guarantees and metadata.

The good news: with that split, the buffer and connector together provide most of this cleanly and the graph just has to participate.

What the Buffer Gives You

For consequential state, SingleLatest is the right default. It has one slot. New value overwrites old. Latest instruction wins.

That isn't a tradeoff between freshness and durability. It's the correct model for instructions:

  • The previous instruction is no longer what the sender means. It was a guess that has been corrected.
  • An older instruction arriving after a newer one is, by definition, stale. Dropping it is correct, not lossy.
  • Whatever ends up in the slot is what gets applied. There is never an unread queue of instructions stacking up while the actuator catches up.

This is freshness and ordering as a buffer property, not as application code. You wrote BufferCfg::SingleLatest. You're done with two of the three.

// Receive setpoints from anywhere on the network. // Latest one wins. Older ones never reach the tap. builder.configure::<Setpoint>(ClimateKey::Setpoint, |reg| { reg.buffer(BufferCfg::SingleLatest) .link_from("mqtt://climate/zone-a/setpoint") .with_qos(QoS::AtLeastOnce) .with_deserializer_raw(Setpoint::from_bytes) .tap(apply_setpoint) .finish(); });

Mailbox is still useful, but as a narrower pattern: destructive consumption with multi-worker semantics, where each worker draws a different task from the slot. For externally driven state, SingleLatest is the clearer model.

What the Connector Gives You

The buffer handles freshness and ordering once a record reaches it. Getting it that far is the connector's job. That's where MQTT's QoS levels or the equivalent on whatever protocol you wrap, become a deliberate choice rather than a default.

QoSWhat it guaranteesWhat it costsWhen to use
0 — At most onceFire and forgetCheap; may drop on flaky linksTelemetry where freshness > completeness
1 — At least onceBroker-ack'd; may duplicateOne round-trip; possible retransmitsDefault for instructions; pair with idempotent application
2 — Exactly onceBroker-ack'd; deduplicatedFour-way handshake; slowRarely worth it — usually QoS 1 + idempotency wins

The honest recommendation for actuation: QoS 1 plus an idempotency strategy in your record. Either the setpoint is idempotent by nature ("target is 21.5°C") and re-applying it changes nothing or you carry a request ID in the record and the tap deduplicates on it. QoS 2 is a tax most systems shouldn't pay. The four-way handshake is real latency and an idempotent application gets you the same end-state guarantee for less money.

What's Missing and Why You Model It as a Record

The buffer gave you freshness and ordering. The connector gave you delivery. Together they get you a record that is current and arrived. They do not get you a record that was applied successfully.

The actuator might be offline. The valve might be stuck. The setpoint might be outside the safe range and silently rejected. None of this is visible to the sender. AimDB does not ship a generic "the consumer applied it" ack channel and adding one as a hidden runtime feature would mean inventing a side-channel that's untyped, undeclared and invisible to the rest of the graph.

The shape that is visible to the rest of the graph is the one AimDB already gives you everywhere else: another record.

#[derive(Clone, Debug, Serialize, Deserialize)] struct SetpointApplied { request_id: u64, target_celsius: f32, actual_celsius: f32, applied_at: u64, outcome: Outcome, } // Inbound — receive the command. SingleLatest + QoS 1. builder.configure::<Setpoint>(ClimateKey::Setpoint, |reg| { reg.buffer(BufferCfg::SingleLatest) .link_from("mqtt://climate/zone-a/setpoint") .with_qos(QoS::AtLeastOnce) .with_deserializer_raw(Setpoint::from_bytes) .finish(); }); // Outbound — publish the receipt. The sender (or anyone) // can see what actually happened, with timestamps. builder.configure::<SetpointApplied>(ClimateKey::Applied, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 64 }) .transform::<Setpoint, _>(ClimateKey::Setpoint, |t| t.map(apply_and_report)) .link_to("mqtt://climate/zone-a/setpoint/applied") .with_serializer_raw(SetpointApplied::to_bytes) .finish(); });

Full working examples are in the examples directory on GitHub.

The ack is just another edge in the graph. The applied-record carries the request ID (so duplicates collapse), the actual outcome (so failures are visible) and a timestamp (so you can compute applied-latency as a regular metric). Anyone who needs to react to a failed setpoint subscribes the same way they'd subscribe to anything else.

This is the shape that scales. It works on a single device. It works across a fleet. It works in a browser dashboard that wants to grey out a button until the device confirms the change. The runtime didn't have to grow a new primitive, the existing primitives composed.

Setpoint round-trip on one node — SingleLatest-buffered command in via link_from, SpmcRing-buffered receipt out via link_to. Same builder, two records, the loop closes through the graph.

What the Graph Actually Owes

Pulling it back together:

  • Freshness — the buffer (usually SingleLatest for state commands). Built into the policy.
  • Ordering — same. Newer overwrites older; older never resurfaces.
  • Delivery — the connector. Choose the QoS that matches how much you care.
  • Idempotency — your record schema. A request ID or naturally idempotent state.
  • Confirmation — another record. The graph has edges. Let the world's reply use one.

What the graph does not owe a consequential consumer is a hidden ack mechanism, a generic retry loop or transactional semantics across nodes. Those are the things that make distributed systems unpredictable, embedded systems brittle and runtimes large. AimDB stays small precisely by making them your model, not its magic.

The Edges That Reach Into the World

A connector that drives output is the graph reaching past its own boundary. The runtime can give it freshness and ordering for free, give it the delivery guarantees you ask for and trace every applied-receipt back to the command that caused it. What it can't do is reach back. That part is yours to model, but the modelling tools are the same records, the same buffers, the same link_to and link_from you've been using all along.

The seam stops being a question and becomes a feature. The world's reply is just another record.

Get Involved

AimDB is open source, Apache 2.0 and growing. SingleLatest + QoS-aware MQTT is shipping today; richer freshness primitives (TTL, request-ID dedup helpers) are on the roadmap.

Star AimDB on GitHub · Live demo · Docs

GitHub stars


Graphics and spell checks in this post were created with the help of an LLM.

Stay in the loop

Get notified about new posts and releases. No spam — unsubscribe anytime.

Powered by Buttondown