diff --git a/examples/purchase.rs b/examples/purchase.rs
index 74a0cd6d..cbe4bfe6 100644
--- a/examples/purchase.rs
+++ b/examples/purchase.rs
@@ -13,10 +13,7 @@
 // ```
 
 use teloxide::{
-    dispatching::{
-        dialogue::{self, InMemStorage},
-        UpdateHandler,
-    },
+    dispatching::{dialogue::InMemStorage, UpdateHandler},
     prelude::*,
     types::{InlineKeyboardButton, InlineKeyboardMarkup},
     utils::command::BotCommands,
@@ -75,12 +72,12 @@ fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>>
         .branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name))
         .branch(dptree::endpoint(invalid_state));
 
-    let callback_query_handler = Update::filter_callback_query().chain(
+    let callback_query_handler = Update::filter_callback_query().branch(
         dptree::case![State::ReceiveProductChoice { full_name }]
             .endpoint(receive_product_selection),
     );
 
-    dialogue::enter::<Update, InMemStorage<State>, State, _>()
+    teloxide::dispatching::dialogue::enter::<Update, InMemStorage<State>, State, _>()
         .branch(message_handler)
         .branch(callback_query_handler)
 }
diff --git a/src/dispatching.rs b/src/dispatching.rs
index 1c093048..042870e8 100644
--- a/src/dispatching.rs
+++ b/src/dispatching.rs
@@ -1,76 +1,175 @@
 //! An update dispatching model based on [`dptree`].
 //!
-//! In teloxide, updates are dispatched by a pipeline. The central type is
-//! [`dptree::Handler`] -- it represents a handler of an update; since the API
-//! is highly declarative, you can combine handlers with each other via such
-//! methods as [`dptree::Handler::chain`] and [`dptree::Handler::branch`]. The
-//! former method pipes one handler to another one, whilst the latter creates a
-//! new node, as communicated by the name. For more information, please refer to
-//! the documentation of [`dptree`].
+//! In teloxide, update dispatching is declarative: it takes the form of a
+//! [chain of responsibility] pattern enriched with a number of combinator
+//! functions, which together form an instance of the [`dptree::Handler`] type.
 //!
-//! The pattern itself is called [chain of responsibility], a well-known design
-//! technique across OOP developers. But unlike typical object-oriented design,
-//! we employ declarative FP-style functions like [`dptree::filter`],
-//! [`dptree::filter_map`], and [`dptree::endpoint`]; these functions create
-//! special forms of [`dptree::Handler`]; for more information, please refer to
-//! their respective documentation. Each of these higher-order functions accept
-//! a closure that is made into a handler -- this closure can take any
-//! additional parameters, which must be supplied while creating [`Dispatcher`]
-//! (see [`DispatcherBuilder::dependencies`]).
-//!
-//! The [`Dispatcher`] type puts all these things together: it only provides
-//! [`Dispatcher::dispatch`] and a handful of other methods. Once you call
-//! `.dispatch()`, it will retrieve updates from the Telegram server and pass
-//! them to your handler, which is a parameter of [`Dispatcher::builder`].
-//!
-//! Let us look at a simple example:
-//!
-//!
-//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/shared_state.rs))
+//! Let us look at this simple example:
 //!
+//! [[`examples/purchase.rs`](https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs)]
 //! ```no_run
-//! // TODO: examples/purchase.rs
-//! fn main() {}
+//! // Imports omitted...
+//! # use teloxide::{
+//! #     dispatching::{dialogue::InMemStorage, UpdateHandler},
+//! #     prelude::*,
+//! #     types::{InlineKeyboardButton, InlineKeyboardMarkup},
+//! #     utils::command::BotCommands,
+//! # };
+//!
+//! type MyDialogue = Dialogue<State, InMemStorage<State>>;
+//! type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
+//!
+//! #[derive(Clone, Default)]
+//! pub enum State {
+//!     #[default]
+//!     Start,
+//!     ReceiveFullName,
+//!     ReceiveProductChoice {
+//!         full_name: String,
+//!     },
+//! }
+//!
+//! #[derive(BotCommands, Clone)]
+//! #[command(rename = "lowercase", description = "These commands are supported:")]
+//! enum Command {
+//!     #[command(description = "display this text.")]
+//!     Help,
+//!     #[command(description = "start the purchase procedure.")]
+//!     Start,
+//!     #[command(description = "cancel the purchase procedure.")]
+//!     Cancel,
+//! }
+//!
+//! #[tokio::main]
+//! async fn main() {
+//!     // Setup is omitted...
+//! #     pretty_env_logger::init();
+//! #     log::info!("Starting purchase bot...");
+//! #
+//! #     let bot = Bot::from_env().auto_send();
+//! #
+//! #     Dispatcher::builder(bot, schema())
+//! #         .dependencies(dptree::deps![InMemStorage::<State>::new()])
+//! #         .build()
+//! #         .setup_ctrlc_handler()
+//! #         .dispatch()
+//! #         .await;
+//! }
+//!
+//! fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
+//!     let command_handler = teloxide::filter_command::<Command, _>()
+//!         .branch(
+//!             dptree::case![State::Start]
+//!                 .branch(dptree::case![Command::Help].endpoint(help))
+//!                 .branch(dptree::case![Command::Start].endpoint(start)),
+//!         )
+//!         .branch(dptree::case![Command::Cancel].endpoint(cancel));
+//!
+//!     let message_handler = Update::filter_message()
+//!         .branch(command_handler)
+//!         .branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name))
+//!         .branch(dptree::endpoint(invalid_state));
+//!
+//!     let callback_query_handler = Update::filter_callback_query().branch(
+//!         dptree::case![State::ReceiveProductChoice { full_name }]
+//!             .endpoint(receive_product_selection),
+//!     );
+//!
+//!     teloxide::dispatching::dialogue::enter::<Update, InMemStorage<State>, State, _>()
+//!         .branch(message_handler)
+//!         .branch(callback_query_handler)
+//! }
+//!
+//! // Handler definitions omitted...
+//!
+//! async fn start(bot: AutoSend<Bot>, msg: Message, dialogue: MyDialogue) -> HandlerResult {
+//!     todo!()
+//! }
+//!
+//! async fn help(bot: AutoSend<Bot>, msg: Message) -> HandlerResult {
+//!     todo!()
+//! }
+//!
+//! async fn cancel(bot: AutoSend<Bot>, msg: Message, dialogue: MyDialogue) -> HandlerResult {
+//!     todo!()
+//! }
+//!
+//! async fn invalid_state(bot: AutoSend<Bot>, msg: Message) -> HandlerResult {
+//!     todo!()
+//! }
+//!
+//! async fn receive_full_name(
+//!     bot: AutoSend<Bot>,
+//!     msg: Message,
+//!     dialogue: MyDialogue,
+//! ) -> HandlerResult {
+//!     todo!()
+//! }
+//!
+//! async fn receive_product_selection(
+//!     bot: AutoSend<Bot>,
+//!     q: CallbackQuery,
+//!     dialogue: MyDialogue,
+//!     full_name: String,
+//! ) -> HandlerResult {
+//!     todo!()
+//! }
 //! ```
 //!
-//!  1. First, we create the bot: `let bot = Bot::from_env().auto_send()`.
-//!  2. Then we construct an update handler. While it is possible to handle all
-//! kinds of [`crate::types::Update`], here we are only interested in
-//! [`crate::types::Message`]: [`UpdateFilterExt::filter_message`] create a
-//! handler object which filters all messages out of a generic update.
-//!  3. By doing `.endpoint(...)` we set up a custom handling closure that
-//! receives `msg: Message` and `bot: AutoSend<Bot>`. There are
-//! called dependencies: `msg` is supplied by
-//! [`UpdateFilterExt::filter_message`], while `bot` is supplied by
-//! [`Dispatcher`].
+//! The above code shows how to dispatch on different combinations of a state
+//! and command _elegantly_. We give a top-bottom explanation of the function
+//! `schema`, which constructs the main update handler:
 //!
-//! That being said, if we receive a message, the dispatcher will call our
-//! handler, but if we receive something other than a message (e.g., a channel
-//! post), you will see an unhandled update notice in your terminal.
+//!  - We call the [`dialogue::enter`] function to initiate dialogue
+//! interaction. Then we call [`dptree::Handler::branch`] two times to form a
+//! tree of responsibility of `message_handler` and `callback_query_handler`.
+//!    - Inside `message_handler`, we use [`Update::filter_message`] as a filter
+//! for incoming messages. Then we create a tree of responsibility again,
+//! consisting of three branches with a similar structure.
+//!    - Inside `callback_query_handler`, we use
+//! [`Update::filter_callback_query`] as a filter and create one branch for
+//! handling product selection.
 //!
-//! This is a very limited example of update pipelining facilities. In more
-//! involved scenarios, there are multiple branches and chains; if one element
-//! of a chain fails to handle an update, the update will be passed forwards; if
-//! no handler succeeds at handling the update, [`Dispatcher`] will invoke a
-//! default handler set up via [`DispatcherBuilder::default_handler`].
+//! `a.branch(b)` roughly means "try to handle an update with `a`, then, if it
+//! fails, try `b`". We use branching multiple times here, which is a natural
+//! pattern for describing dispatching logic. We also use the [`dptree::case!`]
+//! macro extensively, which acts as a filter on an enumeration: if it is of a
+//! certain variant, it passes the variant's payload down the handler chain;
+//! otherwise, it neglects an update. Note how we utilise this macro both for
+//! `State` and `Command` in the same way!
 //!
-//! Update pipelining provides several advantages over the typical `match
-//! (update.kind) { ... }` approach:
+//! Finally, we plug the schema into [`Dispatcher`] like this:
 //!
-//!  1. It supports _extension_: e.g., you
-//! can define extension filters or some other handlers and then combine them in
-//! a single place, thus facilitating loose coupling.
-//!  2. Pipelining exhibits a natural syntax for expressing message processing.
-//!  3. Lastly, it provides a primitive form of [dependency injection (DI)],
-//! which allows you to deal with such objects as a bot and various update types
-//! easily.
+//! ```no_run
+//! # #[tokio::main]
+//! # async fn main() {
+//! let bot = Bot::from_env().auto_send();
 //!
-//! For a more involved example, see [`examples/dispatching_features.rs`](https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs).
+//! Dispatcher::builder(bot, schema())
+//!     .dependencies(dptree::deps![InMemStorage::<State>::new()])
+//!     .build()
+//!     .setup_ctrlc_handler()
+//!     .dispatch()
+//!     .await;
+//! # }
+//! ```
 //!
-//! TODO: explain a more involved example with multiple branches.
+//! In a call to [`DispatcherBuilder::dependencies`], we specify a list of
+//! dependencies that all handlers will receive as parameters. Here, we only
+//! specify an in-memory storage of dialogues needed for [`dialogue::enter`].
+//! However, in production bots, you normally also pass a database connection,
+//! configuration, and other stuff.
 //!
+//! All in all, [`dptree`] can be seen as an extensible alternative to pattern
+//! matching, with support for [dependency injection (DI)] and a few other
+//! useful features. See [`examples/dispatching_features.rs`] as a more involved
+//! example.
+//!
+//! [`Update::filter_message`]: crate::types::Update::filter_message
+//! [`Update::filter_callback_query`]: crate::types::Update::filter_callback_query
 //! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
 //! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection
+//! [`examples/dispatching_features.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs
 
 #[cfg(all(feature = "ctrlc_handler"))]
 pub mod repls;
diff --git a/src/dispatching/dialogue.rs b/src/dispatching/dialogue.rs
index 83b0239d..3af1ecc0 100644
--- a/src/dispatching/dialogue.rs
+++ b/src/dispatching/dialogue.rs
@@ -4,10 +4,11 @@
 //! wrapper over [`Storage`] and a chat ID. All it does is provides convenient
 //! method for manipulating the dialogue state. [`Storage`] is where all
 //! dialogue states are stored; it can be either [`InMemStorage`], which is a
-//! simple hash map, or database wrappers such as [`SqliteStorage`]. In the
-//! latter case, your dialogues are _persistent_, meaning that you can safely
-//! restart your bot and all dialogues will remain in the database -- this is a
-//! preferred method for production bots.
+//! simple hash map from [`std::collections`], or an advanced database wrapper
+//! such as [`SqliteStorage`]. In the latter case, your dialogues are
+//! _persistent_, meaning that you can safely restart your bot and all ongoing
+//! dialogues will remain in the database -- this is a preferred method for
+//! production bots.
 //!
 //! [`examples/dialogue.rs`] clearly demonstrates the typical usage of
 //! dialogues. Your dialogue state can be represented as an enumeration:
@@ -31,8 +32,8 @@
 //!     bot: AutoSend<Bot>,
 //!     msg: Message,
 //!     dialogue: MyDialogue,
-//!     (full_name,): (String,), // Available from `State::ReceiveAge`.
-//! ) -> anyhow::Result<()> {
+//!     full_name: String, // Available from `State::ReceiveAge`.
+//! ) -> HandlerResult {
 //!     match msg.text().map(|text| text.parse::<u8>()) {
 //!         Some(Ok(age)) => {
 //!             bot.send_message(msg.chat.id, "What's your location?").await?;
@@ -47,11 +48,12 @@
 //! }
 //! ```
 //!
-//! Variant's fields are passed to state handlers as tuples: `(full_name,):
-//! (String,)`. Using [`Dialogue::update`], you can update the dialogue with a
-//! new state, in our case -- `State::ReceiveLocation { full_name, age }`. To
-//! exit the dialogue, just call [`Dialogue::exit`] and it will be removed from
-//! the inner storage:
+//! Variant's fields are passed to state handlers as single arguments like
+//! `full_name: String` or tuples in case of two or more variant parameters (see
+//! below). Using [`Dialogue::update`], you can update the dialogue with a new
+//! state, in our case -- `State::ReceiveLocation { full_name, age }`. To exit
+//! the dialogue, just call [`Dialogue::exit`] and it will be removed from the
+//! underlying storage:
 //!
 //! ```ignore
 //! async fn receive_location(