[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-21-event-driven-rust-bus-pattern":3},{"article":4,"author":55},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":35,"related_articles":36},"d0000000-0000-0000-0000-000000000121","a0000000-0000-0000-0000-000000000006","Deep EVM #21: Event-Driven Architecture in Rust — Bus Pattern for Real-Time Systems","deep-evm-21-event-driven-rust-bus-pattern","Design an event-driven system in Rust using tokio channels with the bus pattern. Covers mpsc, broadcast, and watch channels with backpressure and fan-out strategies.","## Why Event-Driven Architecture\n\nIn high-performance systems like MEV bots, blockchain indexers, and real-time trading engines, components must react to events with minimal latency. A new block arrives, and within milliseconds your system must decode transactions, evaluate opportunities, simulate execution, and submit a response.\n\nThe traditional request-response model cannot keep up. You need an event-driven architecture where components communicate through asynchronous message passing. In Rust, tokio's channel primitives provide the foundation for building such systems.\n\n## Tokio Channel Primitives\n\nTokio provides four channel types, each suited for different communication patterns:\n\n### mpsc — Multi-Producer, Single Consumer\n\nMany senders, one receiver. The workhorse for event aggregation:\n\n```rust\nuse tokio::sync::mpsc;\n\n#[derive(Debug, Clone)]\nenum BusEvent {\n    NewBlock { number: u64, hash: [u8; 32] },\n    NewTransaction { hash: [u8; 32], from: [u8; 20], to: [u8; 20] },\n    PriceUpdate { pair: String, price: f64, timestamp: u64 },\n    OpportunityFound { profit_wei: u128, deadline_block: u64 },\n}\n\nasync fn event_aggregator(mut rx: mpsc::Receiver\u003CBusEvent>) {\n    while let Some(event) = rx.recv().await {\n        match event {\n            BusEvent::NewBlock { number, .. } => {\n                tracing::info!(block = number, \"Processing new block\");\n            }\n            BusEvent::PriceUpdate { pair, price, .. } => {\n                tracing::info!(%pair, %price, \"Price updated\");\n            }\n            _ => {}\n        }\n    }\n}\n```\n\nCreate a bounded channel with backpressure:\n\n```rust\nlet (tx, rx) = mpsc::channel::\u003CBusEvent>(1024);\n\n\u002F\u002F Sender blocks if channel is full (backpressure)\ntx.send(BusEvent::NewBlock {\n    number: 18_000_000,\n    hash: [0u8; 32],\n}).await.expect(\"receiver dropped\");\n```\n\n### broadcast — Multi-Producer, Multi-Consumer\n\nEvery subscriber gets every message. Perfect for fan-out:\n\n```rust\nuse tokio::sync::broadcast;\n\nlet (tx, _) = broadcast::channel::\u003CBusEvent>(4096);\n\n\u002F\u002F Each subscriber gets their own receiver\nlet mut rx1 = tx.subscribe();\nlet mut rx2 = tx.subscribe();\n\n\u002F\u002F Publisher\ntx.send(BusEvent::NewBlock {\n    number: 18_000_001,\n    hash: [0u8; 32],\n}).expect(\"no subscribers\");\n\n\u002F\u002F Both rx1 and rx2 receive the event\n```\n\nBroadcast channels have a fixed capacity. If a subscriber falls behind, it receives a `RecvError::Lagged(n)` indicating it missed `n` messages. Handle this gracefully:\n\n```rust\nloop {\n    match rx.recv().await {\n        Ok(event) => handle_event(event).await,\n        Err(broadcast::error::RecvError::Lagged(n)) => {\n            tracing::warn!(missed = n, \"Subscriber lagged, catching up\");\n            \u002F\u002F Continue — next recv() gets the latest available\n        }\n        Err(broadcast::error::RecvError::Closed) => break,\n    }\n}\n```\n\n### watch — Single-Producer, Multi-Consumer (Latest Value)\n\nSubscribers only see the latest value. Ideal for state that changes frequently but consumers only care about the current state:\n\n```rust\nuse tokio::sync::watch;\n\n#[derive(Debug, Clone)]\nstruct SystemState {\n    latest_block: u64,\n    gas_price: u128,\n    connected_peers: usize,\n}\n\nlet (tx, rx) = watch::channel(SystemState {\n    latest_block: 0,\n    gas_price: 0,\n    connected_peers: 0,\n});\n\n\u002F\u002F Update state\ntx.send_modify(|state| {\n    state.latest_block = 18_000_001;\n    state.gas_price = 30_000_000_000; \u002F\u002F 30 gwei\n});\n\n\u002F\u002F Consumer reads latest state\nlet state = rx.borrow();\nprintln!(\"Block: {}, Gas: {} gwei\",\n    state.latest_block,\n    state.gas_price \u002F 1_000_000_000\n);\n```\n\n## The BusEvent Enum Pattern\n\nCentralize all event types in a single enum. This provides type safety, exhaustive pattern matching, and a single source of truth for all events in the system:\n\n```rust\n#[derive(Debug, Clone)]\nenum BusEvent {\n    \u002F\u002F Blockchain events\n    NewBlock(BlockEvent),\n    NewPendingTx(PendingTxEvent),\n    Reorg(ReorgEvent),\n\n    \u002F\u002F Market events\n    PriceUpdate(PriceEvent),\n    LiquidityChange(LiquidityEvent),\n\n    \u002F\u002F System events\n    HealthCheck(HealthEvent),\n    Shutdown,\n}\n\n#[derive(Debug, Clone)]\nstruct BlockEvent {\n    number: u64,\n    hash: [u8; 32],\n    timestamp: u64,\n    base_fee: u128,\n    transactions: Vec\u003C[u8; 32]>,\n}\n```\n\nThis pattern has several advantages:\n- Compile-time guarantee that handlers cover all event types\n- Easy to add new events without breaking existing handlers\n- Serializable for logging and replay\n- Pattern matching enables efficient dispatching\n\n## Handler Registration and Dispatching\n\nBuild a handler registry using trait objects:\n\n```rust\nuse std::sync::Arc;\nuse async_trait::async_trait;\n\n#[async_trait]\ntrait EventHandler: Send + Sync {\n    async fn handle(&self, event: &BusEvent) -> anyhow::Result\u003C()>;\n    fn interested_in(&self, event: &BusEvent) -> bool;\n}\n\nstruct EventBus {\n    tx: broadcast::Sender\u003CBusEvent>,\n    handlers: Vec\u003CArc\u003Cdyn EventHandler>>,\n}\n\nimpl EventBus {\n    fn new(capacity: usize) -> Self {\n        let (tx, _) = broadcast::channel(capacity);\n        Self {\n            tx,\n            handlers: Vec::new(),\n        }\n    }\n\n    fn register(&mut self, handler: Arc\u003Cdyn EventHandler>) {\n        self.handlers.push(handler);\n    }\n\n    async fn start(self) -> anyhow::Result\u003C()> {\n        let mut handles = Vec::new();\n\n        for handler in &self.handlers {\n            let handler = Arc::clone(handler);\n            let mut rx = self.tx.subscribe();\n\n            let handle = tokio::spawn(async move {\n                loop {\n                    match rx.recv().await {\n                        Ok(event) => {\n                            if handler.interested_in(&event) {\n                                if let Err(e) = handler.handle(&event).await {\n                                    tracing::error!(\n                                        error = %e,\n                                        \"Handler failed\"\n                                    );\n                                }\n                            }\n                        }\n                        Err(broadcast::error::RecvError::Lagged(n)) => {\n                            tracing::warn!(missed = n, \"Handler lagged\");\n                        }\n                        Err(broadcast::error::RecvError::Closed) => break,\n                    }\n                }\n            });\n\n            handles.push(handle);\n        }\n\n        futures::future::join_all(handles).await;\n        Ok(())\n    }\n\n    fn publish(&self, event: BusEvent) -> anyhow::Result\u003C()> {\n        self.tx.send(event)\n            .map_err(|_| anyhow::anyhow!(\"No subscribers\"))?;\n        Ok(())\n    }\n}\n```\n\n## Implementing a Concrete Handler\n\n```rust\nstruct ArbitrageDetector {\n    min_profit_wei: u128,\n    result_tx: mpsc::Sender\u003COpportunity>,\n}\n\n#[async_trait]\nimpl EventHandler for ArbitrageDetector {\n    async fn handle(&self, event: &BusEvent) -> anyhow::Result\u003C()> {\n        if let BusEvent::PriceUpdate(price) = event {\n            if let Some(opp) = self.evaluate_arbitrage(price).await? {\n                if opp.profit_wei >= self.min_profit_wei {\n                    self.result_tx.send(opp).await?;\n                }\n            }\n        }\n        Ok(())\n    }\n\n    fn interested_in(&self, event: &BusEvent) -> bool {\n        matches!(event, BusEvent::PriceUpdate(_))\n    }\n}\n```\n\n## Backpressure Strategies\n\nBackpressure is critical in real-time systems. If a handler cannot keep up with events, you need a strategy:\n\n### Strategy 1: Bounded Channel (Block Producer)\n\n```rust\nlet (tx, rx) = mpsc::channel(1024);\n\u002F\u002F If channel is full, send().await blocks the producer\n```\n\nGood for: systems where slowing down the producer is acceptable.\n\n### Strategy 2: Try-Send (Drop Events)\n\n```rust\nmatch tx.try_send(event) {\n    Ok(()) => {},\n    Err(mpsc::error::TrySendError::Full(_)) => {\n        tracing::warn!(\"Channel full, dropping event\");\n        metrics::counter!(\"events_dropped\").increment(1);\n    }\n    Err(mpsc::error::TrySendError::Closed(_)) => break,\n}\n```\n\nGood for: real-time systems where stale events have no value.\n\n### Strategy 3: Replace Latest (Watch Channel)\n\n```rust\nlet (tx, rx) = watch::channel(latest_state);\n\u002F\u002F Only keeps latest value, consumers always see current state\n```\n\nGood for: state that is overwritten frequently (prices, block height).\n\n## When to Use Channels vs Shared State\n\n| Scenario | Use Channels | Use Arc\u003CRwLock\u003CT>> |\n|----------|-------------|---------------------|\n| Event notification | Yes | No |\n| Frequently updated state | watch channel | Yes |\n| Read-heavy, write-rare | No | Yes |\n| Complex transactions | mpsc to actor | Yes (with Mutex) |\n| Fan-out to many consumers | broadcast | No |\n\nThe rule of thumb: if you need to notify others about a change, use channels. If you need to share access to data without notification, use shared state.\n\n## Production Example: MEV Bot Event Flow\n\n```\n[WebSocket] -> NewBlock event -> [broadcast]\n                                    |\n                    +---------------+---------------+\n                    |               |               |\n              [TxDecoder]    [PriceOracle]    [StateUpdater]\n                    |               |               |\n                    v               v               v\n              [mpsc: decoded]  [watch: prices]  [watch: state]\n                    |               |               |\n                    +-------+-------+-------+-------+\n                            |               |\n                    [ArbitrageDetector]  [LiquidationDetector]\n                            |               |\n                            v               v\n                    [mpsc: opportunities]\n                            |\n                    [ExecutionEngine]\n```\n\nThis architecture processes a new block in under 10ms with each component running independently on separate tokio tasks.\n\n## Conclusion\n\nEvent-driven architecture in Rust with tokio channels provides the performance and type safety needed for real-time systems. The BusEvent enum pattern centralizes event definitions, broadcast channels enable fan-out, mpsc channels provide backpressure, and watch channels optimize for latest-value semantics. Choose the right channel type for each communication pattern, implement proper backpressure handling, and your system will be both fast and resilient.","\u003Ch2 id=\"why-event-driven-architecture\">Why Event-Driven Architecture\u003C\u002Fh2>\n\u003Cp>In high-performance systems like MEV bots, blockchain indexers, and real-time trading engines, components must react to events with minimal latency. A new block arrives, and within milliseconds your system must decode transactions, evaluate opportunities, simulate execution, and submit a response.\u003C\u002Fp>\n\u003Cp>The traditional request-response model cannot keep up. You need an event-driven architecture where components communicate through asynchronous message passing. In Rust, tokio’s channel primitives provide the foundation for building such systems.\u003C\u002Fp>\n\u003Ch2 id=\"tokio-channel-primitives\">Tokio Channel Primitives\u003C\u002Fh2>\n\u003Cp>Tokio provides four channel types, each suited for different communication patterns:\u003C\u002Fp>\n\u003Ch3>mpsc — Multi-Producer, Single Consumer\u003C\u002Fh3>\n\u003Cp>Many senders, one receiver. The workhorse for event aggregation:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::sync::mpsc;\n\n#[derive(Debug, Clone)]\nenum BusEvent {\n    NewBlock { number: u64, hash: [u8; 32] },\n    NewTransaction { hash: [u8; 32], from: [u8; 20], to: [u8; 20] },\n    PriceUpdate { pair: String, price: f64, timestamp: u64 },\n    OpportunityFound { profit_wei: u128, deadline_block: u64 },\n}\n\nasync fn event_aggregator(mut rx: mpsc::Receiver&lt;BusEvent&gt;) {\n    while let Some(event) = rx.recv().await {\n        match event {\n            BusEvent::NewBlock { number, .. } =&gt; {\n                tracing::info!(block = number, \"Processing new block\");\n            }\n            BusEvent::PriceUpdate { pair, price, .. } =&gt; {\n                tracing::info!(%pair, %price, \"Price updated\");\n            }\n            _ =&gt; {}\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Create a bounded channel with backpressure:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let (tx, rx) = mpsc::channel::&lt;BusEvent&gt;(1024);\n\n\u002F\u002F Sender blocks if channel is full (backpressure)\ntx.send(BusEvent::NewBlock {\n    number: 18_000_000,\n    hash: [0u8; 32],\n}).await.expect(\"receiver dropped\");\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>broadcast — Multi-Producer, Multi-Consumer\u003C\u002Fh3>\n\u003Cp>Every subscriber gets every message. Perfect for fan-out:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::sync::broadcast;\n\nlet (tx, _) = broadcast::channel::&lt;BusEvent&gt;(4096);\n\n\u002F\u002F Each subscriber gets their own receiver\nlet mut rx1 = tx.subscribe();\nlet mut rx2 = tx.subscribe();\n\n\u002F\u002F Publisher\ntx.send(BusEvent::NewBlock {\n    number: 18_000_001,\n    hash: [0u8; 32],\n}).expect(\"no subscribers\");\n\n\u002F\u002F Both rx1 and rx2 receive the event\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Broadcast channels have a fixed capacity. If a subscriber falls behind, it receives a \u003Ccode>RecvError::Lagged(n)\u003C\u002Fcode> indicating it missed \u003Ccode>n\u003C\u002Fcode> messages. Handle this gracefully:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">loop {\n    match rx.recv().await {\n        Ok(event) =&gt; handle_event(event).await,\n        Err(broadcast::error::RecvError::Lagged(n)) =&gt; {\n            tracing::warn!(missed = n, \"Subscriber lagged, catching up\");\n            \u002F\u002F Continue — next recv() gets the latest available\n        }\n        Err(broadcast::error::RecvError::Closed) =&gt; break,\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>watch — Single-Producer, Multi-Consumer (Latest Value)\u003C\u002Fh3>\n\u003Cp>Subscribers only see the latest value. Ideal for state that changes frequently but consumers only care about the current state:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use tokio::sync::watch;\n\n#[derive(Debug, Clone)]\nstruct SystemState {\n    latest_block: u64,\n    gas_price: u128,\n    connected_peers: usize,\n}\n\nlet (tx, rx) = watch::channel(SystemState {\n    latest_block: 0,\n    gas_price: 0,\n    connected_peers: 0,\n});\n\n\u002F\u002F Update state\ntx.send_modify(|state| {\n    state.latest_block = 18_000_001;\n    state.gas_price = 30_000_000_000; \u002F\u002F 30 gwei\n});\n\n\u002F\u002F Consumer reads latest state\nlet state = rx.borrow();\nprintln!(\"Block: {}, Gas: {} gwei\",\n    state.latest_block,\n    state.gas_price \u002F 1_000_000_000\n);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-busevent-enum-pattern\">The BusEvent Enum Pattern\u003C\u002Fh2>\n\u003Cp>Centralize all event types in a single enum. This provides type safety, exhaustive pattern matching, and a single source of truth for all events in the system:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[derive(Debug, Clone)]\nenum BusEvent {\n    \u002F\u002F Blockchain events\n    NewBlock(BlockEvent),\n    NewPendingTx(PendingTxEvent),\n    Reorg(ReorgEvent),\n\n    \u002F\u002F Market events\n    PriceUpdate(PriceEvent),\n    LiquidityChange(LiquidityEvent),\n\n    \u002F\u002F System events\n    HealthCheck(HealthEvent),\n    Shutdown,\n}\n\n#[derive(Debug, Clone)]\nstruct BlockEvent {\n    number: u64,\n    hash: [u8; 32],\n    timestamp: u64,\n    base_fee: u128,\n    transactions: Vec&lt;[u8; 32]&gt;,\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This pattern has several advantages:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Compile-time guarantee that handlers cover all event types\u003C\u002Fli>\n\u003Cli>Easy to add new events without breaking existing handlers\u003C\u002Fli>\n\u003Cli>Serializable for logging and replay\u003C\u002Fli>\n\u003Cli>Pattern matching enables efficient dispatching\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"handler-registration-and-dispatching\">Handler Registration and Dispatching\u003C\u002Fh2>\n\u003Cp>Build a handler registry using trait objects:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::sync::Arc;\nuse async_trait::async_trait;\n\n#[async_trait]\ntrait EventHandler: Send + Sync {\n    async fn handle(&amp;self, event: &amp;BusEvent) -&gt; anyhow::Result&lt;()&gt;;\n    fn interested_in(&amp;self, event: &amp;BusEvent) -&gt; bool;\n}\n\nstruct EventBus {\n    tx: broadcast::Sender&lt;BusEvent&gt;,\n    handlers: Vec&lt;Arc&lt;dyn EventHandler&gt;&gt;,\n}\n\nimpl EventBus {\n    fn new(capacity: usize) -&gt; Self {\n        let (tx, _) = broadcast::channel(capacity);\n        Self {\n            tx,\n            handlers: Vec::new(),\n        }\n    }\n\n    fn register(&amp;mut self, handler: Arc&lt;dyn EventHandler&gt;) {\n        self.handlers.push(handler);\n    }\n\n    async fn start(self) -&gt; anyhow::Result&lt;()&gt; {\n        let mut handles = Vec::new();\n\n        for handler in &amp;self.handlers {\n            let handler = Arc::clone(handler);\n            let mut rx = self.tx.subscribe();\n\n            let handle = tokio::spawn(async move {\n                loop {\n                    match rx.recv().await {\n                        Ok(event) =&gt; {\n                            if handler.interested_in(&amp;event) {\n                                if let Err(e) = handler.handle(&amp;event).await {\n                                    tracing::error!(\n                                        error = %e,\n                                        \"Handler failed\"\n                                    );\n                                }\n                            }\n                        }\n                        Err(broadcast::error::RecvError::Lagged(n)) =&gt; {\n                            tracing::warn!(missed = n, \"Handler lagged\");\n                        }\n                        Err(broadcast::error::RecvError::Closed) =&gt; break,\n                    }\n                }\n            });\n\n            handles.push(handle);\n        }\n\n        futures::future::join_all(handles).await;\n        Ok(())\n    }\n\n    fn publish(&amp;self, event: BusEvent) -&gt; anyhow::Result&lt;()&gt; {\n        self.tx.send(event)\n            .map_err(|_| anyhow::anyhow!(\"No subscribers\"))?;\n        Ok(())\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"implementing-a-concrete-handler\">Implementing a Concrete Handler\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct ArbitrageDetector {\n    min_profit_wei: u128,\n    result_tx: mpsc::Sender&lt;Opportunity&gt;,\n}\n\n#[async_trait]\nimpl EventHandler for ArbitrageDetector {\n    async fn handle(&amp;self, event: &amp;BusEvent) -&gt; anyhow::Result&lt;()&gt; {\n        if let BusEvent::PriceUpdate(price) = event {\n            if let Some(opp) = self.evaluate_arbitrage(price).await? {\n                if opp.profit_wei &gt;= self.min_profit_wei {\n                    self.result_tx.send(opp).await?;\n                }\n            }\n        }\n        Ok(())\n    }\n\n    fn interested_in(&amp;self, event: &amp;BusEvent) -&gt; bool {\n        matches!(event, BusEvent::PriceUpdate(_))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"backpressure-strategies\">Backpressure Strategies\u003C\u002Fh2>\n\u003Cp>Backpressure is critical in real-time systems. If a handler cannot keep up with events, you need a strategy:\u003C\u002Fp>\n\u003Ch3>Strategy 1: Bounded Channel (Block Producer)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">let (tx, rx) = mpsc::channel(1024);\n\u002F\u002F If channel is full, send().await blocks the producer\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Good for: systems where slowing down the producer is acceptable.\u003C\u002Fp>\n\u003Ch3>Strategy 2: Try-Send (Drop Events)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">match tx.try_send(event) {\n    Ok(()) =&gt; {},\n    Err(mpsc::error::TrySendError::Full(_)) =&gt; {\n        tracing::warn!(\"Channel full, dropping event\");\n        metrics::counter!(\"events_dropped\").increment(1);\n    }\n    Err(mpsc::error::TrySendError::Closed(_)) =&gt; break,\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Good for: real-time systems where stale events have no value.\u003C\u002Fp>\n\u003Ch3>Strategy 3: Replace Latest (Watch Channel)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">let (tx, rx) = watch::channel(latest_state);\n\u002F\u002F Only keeps latest value, consumers always see current state\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Good for: state that is overwritten frequently (prices, block height).\u003C\u002Fp>\n\u003Ch2 id=\"when-to-use-channels-vs-shared-state\">When to Use Channels vs Shared State\u003C\u002Fh2>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Scenario\u003C\u002Fth>\u003Cth>Use Channels\u003C\u002Fth>\u003Cth>Use Arc&lt;RwLock\u003CT>&gt;\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Event notification\u003C\u002Ftd>\u003Ctd>Yes\u003C\u002Ftd>\u003Ctd>No\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Frequently updated state\u003C\u002Ftd>\u003Ctd>watch channel\u003C\u002Ftd>\u003Ctd>Yes\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Read-heavy, write-rare\u003C\u002Ftd>\u003Ctd>No\u003C\u002Ftd>\u003Ctd>Yes\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Complex transactions\u003C\u002Ftd>\u003Ctd>mpsc to actor\u003C\u002Ftd>\u003Ctd>Yes (with Mutex)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Fan-out to many consumers\u003C\u002Ftd>\u003Ctd>broadcast\u003C\u002Ftd>\u003Ctd>No\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The rule of thumb: if you need to notify others about a change, use channels. If you need to share access to data without notification, use shared state.\u003C\u002Fp>\n\u003Ch2 id=\"production-example-mev-bot-event-flow\">Production Example: MEV Bot Event Flow\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>[WebSocket] -&gt; NewBlock event -&gt; [broadcast]\n                                    |\n                    +---------------+---------------+\n                    |               |               |\n              [TxDecoder]    [PriceOracle]    [StateUpdater]\n                    |               |               |\n                    v               v               v\n              [mpsc: decoded]  [watch: prices]  [watch: state]\n                    |               |               |\n                    +-------+-------+-------+-------+\n                            |               |\n                    [ArbitrageDetector]  [LiquidationDetector]\n                            |               |\n                            v               v\n                    [mpsc: opportunities]\n                            |\n                    [ExecutionEngine]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This architecture processes a new block in under 10ms with each component running independently on separate tokio tasks.\u003C\u002Fp>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Event-driven architecture in Rust with tokio channels provides the performance and type safety needed for real-time systems. The BusEvent enum pattern centralizes event definitions, broadcast channels enable fan-out, mpsc channels provide backpressure, and watch channels optimize for latest-value semantics. Choose the right channel type for each communication pattern, implement proper backpressure handling, and your system will be both fast and resilient.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.140613Z","Event-Driven Architecture in Rust — Bus Pattern for Real-Time Systems","Design event-driven systems in Rust using tokio channels with mpsc, broadcast, and watch patterns for real-time performance.","event driven rust tokio channels",null,"index, follow",[22,27,31],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust","Engineering",[37,43,49],{"id":38,"title":39,"slug":40,"excerpt":41,"locale":12,"category_name":35,"published_at":42},"d0200000-0000-0000-0000-000000000003","Why Bali Is Becoming Southeast Asia's Impact-Tech Hub in 2026","why-bali-becoming-southeast-asia-impact-tech-hub-2026","Bali ranks #16 among Southeast Asian startup ecosystems. With a growing concentration of Web3 builders, AI sustainability startups, and eco-travel tech companies, the island is carving a niche as the region's impact-tech capital.","2026-03-28T10:44:37.748283Z",{"id":44,"title":45,"slug":46,"excerpt":47,"locale":12,"category_name":35,"published_at":48},"d0200000-0000-0000-0000-000000000002","ASEAN Data Protection Patchwork: A Developer's Compliance Checklist","asean-data-protection-patchwork-developer-compliance-checklist","Seven ASEAN countries now have comprehensive data protection laws, each with different consent models, localization requirements, and penalty structures. Here is a practical compliance checklist for developers building multi-country applications.","2026-03-28T10:44:37.374741Z",{"id":50,"title":51,"slug":52,"excerpt":53,"locale":12,"category_name":35,"published_at":54},"d0200000-0000-0000-0000-000000000001","Indonesia's $29 Billion Digital Transformation: Opportunities for Software Companies","indonesia-29-billion-digital-transformation-opportunities-software-companies","Indonesia's IT services market is projected to reach $29.03 billion in 2026, up from $24.37 billion in 2025. Cloud infrastructure, AI, e-commerce, and data centers are driving the fastest growth in Southeast Asia.","2026-03-28T10:44:37.349311Z",{"id":13,"name":56,"slug":57,"bio":58,"photo_url":19,"linkedin":19,"role":59,"created_at":60,"updated_at":60},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]