<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex</title>
    <description>The latest articles on DEV Community by Alex (@sounds-like-lx).</description>
    <link>https://dev.to/sounds-like-lx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3907948%2F47211ccf-8ea5-41f8-8409-b1fa8b5a207e.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/sounds-like-lx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sounds-like-lx"/>
    <language>en</language>
    <item>
      <title>From Spawn to FuturesUnordered: How Community Feedback Reshaped Our Async Design</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 09 May 2026 19:15:44 +0000</pubDate>
      <link>https://dev.to/sounds-like-lx/from-spawn-to-futuresunordered-how-community-feedback-reshaped-our-async-design-2lpa</link>
      <guid>https://dev.to/sounds-like-lx/from-spawn-to-futuresunordered-how-community-feedback-reshaped-our-async-design-2lpa</guid>
      <description>&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I published a proof-of-concept on Reddit: &lt;strong&gt;"Abstraction as Complement to Standardization."&lt;/strong&gt; The core idea: avoid vendor lock-in not with more layers, but by building a thin trait abstraction directly on top of the &lt;code&gt;Future&lt;/code&gt; standard that Tokio, Embassy and WASM all implement.&lt;/p&gt;

&lt;p&gt;The post resonated — it hit #2 on r/rust. But the real value came afterward, in the comments.&lt;/p&gt;

&lt;p&gt;The community didn't just validate the approach. They questioned a specific design decision and offered something cleaner. And now we're rethinking the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Design: Four Traits
&lt;/h2&gt;

&lt;p&gt;The PoC proposed four traits composed into a generic &lt;code&gt;Runtime&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RuntimeAdapter&lt;/code&gt; — platform identity&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Spawn&lt;/code&gt; — task creation (the controversial one)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TimeOps&lt;/code&gt; — clocks and sleep&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Logger&lt;/code&gt; — structured output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setup was clean: portable code receives &lt;code&gt;RuntimeContext&amp;lt;R&amp;gt;&lt;/code&gt; and uses typed accessors. Everything monomorphizes away at compile time. No vtables, no dynamic dispatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;heartbeat&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RuntimeContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;log&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.log&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="nf"&gt;.sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="nf"&gt;.secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="nf"&gt;.info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"alive"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But there were two trade-offs baked in:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade-off #1: &lt;code&gt;Send + Sync&lt;/code&gt; Bounds Leak Into Single-Threaded Targets
&lt;/h3&gt;

&lt;p&gt;The traits assumed the most demanding target (multi-threaded Tokio). Embassy and WASM needed scoped &lt;code&gt;unsafe impl Send/Sync&lt;/code&gt; as a workaround — justified but inelegant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade-off #2: Fighting Embassy's Philosophy
&lt;/h3&gt;

&lt;p&gt;Embassy is idiomatic at full static declaration. Our &lt;code&gt;Spawn&lt;/code&gt; trait forced dynamic behavior through a type-erased task pool with &lt;code&gt;unsafe { Pin::new_unchecked(...) }&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BoxedFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;'static&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[embassy_executor::task(pool_size&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;TASK_POOL_SIZE)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generic_task_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BoxedFuture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Pin&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_unchecked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;unsafe&lt;/code&gt; is sound. But it works &lt;em&gt;against&lt;/em&gt; Embassy's design, not with it.&lt;/p&gt;

&lt;p&gt;I noted both trade-offs in the post. I called them "not yet happy with." Then I published and watched the responses come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Community Found
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/rust/..." rel="noopener noreferrer"&gt;&lt;strong&gt;Read the full Reddit thread here.&lt;/strong&gt;&lt;/a&gt; The discussion is worth your time — people contributed genuinely thoughtful patterns.&lt;/p&gt;

&lt;p&gt;But one insight kept surfacing, from multiple angles: &lt;strong&gt;Maybe &lt;code&gt;Spawn&lt;/code&gt; shouldn't be a default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The realization came from a simple observation: &lt;strong&gt;When does AimDB actually spawn tasks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only during &lt;code&gt;database.build()&lt;/code&gt;. After initialization completes, no new tasks are spawned. The set of futures is fully determined by configuration, not runtime events.&lt;/p&gt;

&lt;p&gt;That's a constraint we didn't design around. We carried the &lt;code&gt;Spawn&lt;/code&gt; abstraction for all runtimes because it's needed on Tokio. But on Embassy and WASM, it's pure overhead. A workaround solving a non-problem.&lt;/p&gt;

&lt;p&gt;What if we used a different primitive for that specific phase?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: &lt;code&gt;FuturesUnordered&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FuturesUnordered&lt;/code&gt; from the &lt;code&gt;futures&lt;/code&gt; crate is designed for exactly this: managing a dynamic set of heterogeneous futures without an executor-level spawn primitive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FuturesUnordered&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;producer_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;consumer_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;transform_forwarder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No executor-level &lt;code&gt;Spawn&lt;/code&gt; trait needed&lt;/li&gt;
&lt;li&gt;✅ No &lt;code&gt;'static&lt;/code&gt; cliff forcing &lt;code&gt;unsafe impl Send/Sync&lt;/code&gt; workarounds&lt;/li&gt;
&lt;li&gt;✅ No type-erased boxing to fight Embassy's static nature&lt;/li&gt;
&lt;li&gt;✅ Works identically on Tokio, Embassy, WASM&lt;/li&gt;
&lt;li&gt;✅ Heterogeneous futures still boxed, but that's internal bookkeeping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The trade-off:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You call &lt;code&gt;database.run().await&lt;/code&gt; instead of spawning tasks and letting the executor find them&lt;/li&gt;
&lt;li&gt;But you get back: simpler trait bound, no unsafe, no platform-specific hacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This isn't about one design being "right" and another "wrong." It's about &lt;strong&gt;constraints shaping architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We optimized for the general case (multi-core work-stealing on Tokio). Then we adapted that design to single-threaded targets by layering workarounds on top.&lt;/p&gt;

&lt;p&gt;What if we'd started from: "What's the minimal, idiomatic way to drive tasks on &lt;em&gt;each&lt;/em&gt; platform?" Then composed backward to the abstraction layer?&lt;/p&gt;

&lt;p&gt;That's FuturesUnordered for Embassy. That's work-stealing executors for Tokio. Then the common abstraction lives &lt;em&gt;at the interface&lt;/em&gt;, not buried inside.&lt;/p&gt;

&lt;p&gt;The community's feedback wasn't "your design is wrong." It was: "You're solving a problem that exists only because of how you framed the solution." This is what I want to emphasize: &lt;strong&gt;the community made us think differently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not about the final implementation. That's TBD, but about &lt;em&gt;how&lt;/em&gt; to think about cross-platform abstractions.&lt;/p&gt;

&lt;p&gt;The anti-pattern we uncovered: &lt;strong&gt;Optimize for the richest platform, then add scoped workarounds for simpler ones.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern to try: &lt;strong&gt;Start from the constraints of each platform. Build the abstraction at the interface layer where they meet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a subtle shift. But it changes where you place the burden. Instead of Platform A imposing its model on Platforms B and C, each platform expresses its nature clearly and the abstraction is the &lt;em&gt;minimal&lt;/em&gt; glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;We're evaluating the FuturesUnordered pivot. If it works (and early signals suggest it does), we'll:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the runtime adapter trait bundle&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;unsafe impl Send/Sync&lt;/code&gt; scoping&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;database.run().await&lt;/code&gt; as the initialization API&lt;/li&gt;
&lt;li&gt;Write up the design decision and rationale&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But here's the key: &lt;strong&gt;None of this happens without the Reddit conversation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Thoughts?
&lt;/h2&gt;

&lt;p&gt;If you've built cross-platform abstractions in Rust, what's your instinct? Does starting from platform constraints instead of feature parity change how you'd approach it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're actively tracking this work here:&lt;/strong&gt; &lt;a href="https://github.com/orgs/aimdb-dev/projects/3?pane=issue&amp;amp;itemId=184468441&amp;amp;issue=aimdb-dev%7Caimdb%7C88" rel="noopener noreferrer"&gt;GitHub Issue #88 — Reconsider Spawn as default in runtime adapter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to follow the implementation, jump in with thoughts, or hit us with pattern suggestions you've seen work, that's the place.&lt;/p&gt;

&lt;p&gt;This is the kind of thinking-in-public I want to keep doing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.reddit.com/r/rust/..." rel="noopener noreferrer"&gt;Reddit&lt;/a&gt; as "Abstraction as Complement to Standardization." This is a follow-up reflecting on how the community feedback shaped our thinking.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>community</category>
    </item>
  </channel>
</rss>
