You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2026/03/26 15:38:11 UTC

(iggy) branch master updated: refactor(server): replace ServerCommand enum and binary mapper with wire types (#3027)

This is an automated email from the ASF dual-hosted git repository.

hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new f8be02437 refactor(server): replace ServerCommand enum and binary mapper with wire types (#3027)
f8be02437 is described below

commit f8be02437b0bea1d035c5caaa0cc324386a8e8dd
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Thu Mar 26 16:38:06 2026 +0100

    refactor(server): replace ServerCommand enum and binary mapper with wire types (#3027)
---
 Cargo.lock                                         |  16 +-
 Cargo.toml                                         |   1 -
 DEPENDENCIES.md                                    |   1 -
 core/binary_protocol/src/framing.rs                |  52 +-
 .../src/requests/system/get_snapshot.rs            |  98 +++-
 .../src/requests/users/create_user.rs              |  18 +-
 .../src/requests/users/update_permissions.rs       |  14 +-
 core/integration/Cargo.toml                        |   2 +-
 .../server/scenarios/authentication_scenario.rs    | 349 +++++--------
 core/server/Cargo.toml                             |   2 +-
 core/server/src/binary/command.rs                  | 432 ---------------
 core/server/src/binary/dispatch.rs                 | 581 +++++++++++++++++++++
 .../cluster/get_cluster_metadata_handler.rs        |  72 ++-
 .../create_consumer_group_handler.rs               |  88 ++--
 .../delete_consumer_group_handler.rs               |  73 ++-
 .../consumer_groups/get_consumer_group_handler.rs  |  97 ++--
 .../consumer_groups/get_consumer_groups_handler.rs |  86 ++-
 .../consumer_groups/join_consumer_group_handler.rs |  74 ++-
 .../leave_consumer_group_handler.rs                |  74 ++-
 .../delete_consumer_offset_handler.rs              |  76 +--
 .../get_consumer_offset_handler.rs                 | 108 ++--
 .../store_consumer_offset_handler.rs               |  86 ++-
 .../messages/flush_unsaved_buffer_handler.rs       |  93 ++--
 .../handlers/messages/poll_messages_handler.rs     | 128 ++---
 .../handlers/messages/send_messages_handler.rs     | 343 ++++++------
 core/server/src/binary/handlers/mod.rs             |   1 -
 .../partitions/create_partitions_handler.rs        |  74 ++-
 .../partitions/delete_partitions_handler.rs        |  74 ++-
 .../create_personal_access_token_handler.rs        |  85 ++-
 .../delete_personal_access_token_handler.rs        |  75 ++-
 .../get_personal_access_tokens_handler.rs          |  79 ++-
 .../login_with_personal_access_token_handler.rs    |  78 ++-
 .../handlers/segments/delete_segments_handler.rs   | 101 ++--
 core/server/src/binary/handlers/segments/mod.rs    |   2 +-
 .../handlers/streams/create_stream_handler.rs      |  86 ++-
 .../handlers/streams/delete_stream_handler.rs      |  68 +--
 .../binary/handlers/streams/get_stream_handler.rs  | 126 +++--
 .../binary/handlers/streams/get_streams_handler.rs |  68 ++-
 .../handlers/streams/purge_stream_handler.rs       |  68 +--
 .../handlers/streams/update_stream_handler.rs      |  74 ++-
 .../binary/handlers/system/get_client_handler.rs   |  67 +--
 .../binary/handlers/system/get_clients_handler.rs  |  57 +-
 .../src/binary/handlers/system/get_me_handler.rs   |  79 +--
 .../binary/handlers/system/get_snapshot_handler.rs |  60 +--
 .../binary/handlers/system/get_stats_handler.rs    | 105 ++--
 .../src/binary/handlers/system/ping_handler.rs     |  48 +-
 .../binary/handlers/topics/create_topic_handler.rs | 126 +++--
 .../binary/handlers/topics/delete_topic_handler.rs |  70 +--
 .../binary/handlers/topics/get_topic_handler.rs    |  92 ++--
 .../binary/handlers/topics/get_topics_handler.rs   |  74 ++-
 .../binary/handlers/topics/purge_topic_handler.rs  |  70 +--
 .../binary/handlers/topics/update_topic_handler.rs |  84 +--
 .../handlers/users/change_password_handler.rs      |  84 ++-
 .../binary/handlers/users/create_user_handler.rs   |  97 ++--
 .../binary/handlers/users/delete_user_handler.rs   |  70 +--
 .../src/binary/handlers/users/get_user_handler.rs  |  78 ++-
 .../src/binary/handlers/users/get_users_handler.rs |  67 +--
 .../binary/handlers/users/login_user_handler.rs    |  88 ++--
 .../binary/handlers/users/logout_user_handler.rs   |  65 +--
 .../handlers/users/update_permissions_handler.rs   |  88 ++--
 .../binary/handlers/users/update_user_handler.rs   |  78 ++-
 core/server/src/binary/handlers/utils.rs           |  43 --
 core/server/src/binary/macros.rs                   | 114 ----
 core/server/src/binary/mapper.rs                   | 431 ---------------
 core/server/src/binary/mod.rs                      |   6 +-
 core/server/src/http/partitions.rs                 |   4 +-
 core/server/src/http/streams.rs                    |   2 +-
 core/server/src/http/topics.rs                     |   2 +-
 core/server/src/quic/listener.rs                   |  52 +-
 core/server/src/shard/execution.rs                 |  43 +-
 core/server/src/shard/handlers.rs                  |  20 +-
 core/server/src/shard/transmission/frame.rs        |   8 +-
 core/server/src/tcp/connection_handler.rs          |  82 +--
 core/server/src/websocket/connection_handler.rs    | 136 ++---
 74 files changed, 2916 insertions(+), 3697 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 02b353962..827bef7a4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3428,18 +3428,6 @@ dependencies = [
  "syn 2.0.117",
 ]
 
-[[package]]
-name = "enum_dispatch"
-version = "0.3.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
-dependencies = [
- "once_cell",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
 [[package]]
 name = "enumset"
 version = "1.1.10"
@@ -5840,6 +5828,7 @@ dependencies = [
  "humantime",
  "iggy",
  "iggy-cli",
+ "iggy_binary_protocol",
  "iggy_common",
  "iggy_connector_sdk",
  "keyring",
@@ -5861,7 +5850,6 @@ dependencies = [
  "server",
  "socket2 0.6.3",
  "sqlx",
- "strum 0.28.0",
  "sysinfo 0.38.4",
  "tempfile",
  "test-case",
@@ -9743,7 +9731,6 @@ dependencies = [
  "cyper-axum",
  "dashmap",
  "dotenvy",
- "enum_dispatch",
  "err_trail",
  "error_set",
  "figlet-rs",
@@ -9753,6 +9740,7 @@ dependencies = [
  "hash32 1.0.0",
  "human-repr",
  "hwlocality",
+ "iggy_binary_protocol",
  "iggy_common",
  "jsonwebtoken",
  "left-right",
diff --git a/Cargo.toml b/Cargo.toml
index 0f8142ef2..5918468b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -135,7 +135,6 @@ dirs = "6.0.0"
 dlopen2 = "0.8.2"
 dotenvy = "0.15.7"
 elasticsearch = { version = "9.1.0-alpha.1", features = ["rustls-tls"], default-features = false }
-enum_dispatch = "0.3.13"
 enumset = "1.1"
 env_logger = "0.11.9"
 err_trail = { version = "0.11.0", features = ["tracing"] }
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 83d9349ad..b6100cdfb 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -297,7 +297,6 @@ embedded-io: 0.6.1, "Apache-2.0 OR MIT",
 encode_unicode: 1.0.0, "Apache-2.0 OR MIT",
 encoding_rs: 0.8.35, "(Apache-2.0 OR MIT) AND BSD-3-Clause",
 enum-as-inner: 0.6.1, "Apache-2.0 OR MIT",
-enum_dispatch: 0.3.13, "Apache-2.0 OR MIT",
 enumset: 1.1.10, "Apache-2.0 OR MIT",
 enumset_derive: 0.14.0, "Apache-2.0 OR MIT",
 equator: 0.4.2, "MIT",
diff --git a/core/binary_protocol/src/framing.rs b/core/binary_protocol/src/framing.rs
index 981ead667..f303a61ae 100644
--- a/core/binary_protocol/src/framing.rs
+++ b/core/binary_protocol/src/framing.rs
@@ -48,19 +48,40 @@ impl<'a> RequestFrame<'a> {
     /// Size of the frame header: `[length:4][code:4]`.
     pub const HEADER_SIZE: usize = 8;
 
+    /// Validate a frame length field and return the payload size.
+    ///
+    /// Transport layers that read the length and code fields incrementally
+    /// (e.g. compio completion-based I/O) can use this to validate the length
+    /// before reading the payload, without buffering the entire frame.
+    ///
+    /// # Errors
+    /// Returns `WireError::Validation` if `frame_length < 4` (too small to
+    /// contain even the command code).
+    pub fn payload_length(frame_length: u32) -> Result<u32, WireError> {
+        frame_length
+            .checked_sub(4)
+            .ok_or(WireError::Validation(Cow::Borrowed(
+                "request frame length must be at least 4 (code size)",
+            )))
+    }
+
+    /// Construct a frame from pre-parsed header fields and a payload slice.
+    ///
+    /// Used by transport layers that read the header incrementally (e.g.
+    /// compio completion-based I/O) and then read the payload separately.
+    #[must_use]
+    pub const fn from_parts(code: u32, payload: &'a [u8]) -> Self {
+        Self { code, payload }
+    }
+
     /// Decode a request frame from a complete buffer.
     ///
     /// # Errors
     /// Returns `WireError::UnexpectedEof` if the buffer is too short.
     pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
-        let length = read_u32_le(buf, 0)? as usize;
-        if length < 4 {
-            return Err(WireError::Validation(Cow::Borrowed(
-                "request frame length must be at least 4 (code size)",
-            )));
-        }
+        let frame_length = read_u32_le(buf, 0)?;
+        let payload_len = Self::payload_length(frame_length)? as usize;
         let code = read_u32_le(buf, 4)?;
-        let payload_len = length - 4;
         let payload = read_bytes(buf, Self::HEADER_SIZE, payload_len)?;
         let total = Self::HEADER_SIZE + payload_len;
         Ok((Self { code, payload }, total))
@@ -222,6 +243,23 @@ mod tests {
         assert!(RequestFrame::decode(&buf).is_err());
     }
 
+    #[test]
+    fn payload_length_valid() {
+        assert_eq!(RequestFrame::payload_length(4).unwrap(), 0);
+        assert_eq!(RequestFrame::payload_length(104).unwrap(), 100);
+        assert_eq!(
+            RequestFrame::payload_length(u32::MAX).unwrap(),
+            u32::MAX - 4
+        );
+    }
+
+    #[test]
+    fn payload_length_too_small() {
+        assert!(RequestFrame::payload_length(0).is_err());
+        assert!(RequestFrame::payload_length(1).is_err());
+        assert!(RequestFrame::payload_length(3).is_err());
+    }
+
     #[test]
     fn request_encoded_size() {
         assert_eq!(RequestFrame::encoded_size(0), Some(8));
diff --git a/core/binary_protocol/src/requests/system/get_snapshot.rs b/core/binary_protocol/src/requests/system/get_snapshot.rs
index 4509158ff..0342cde3f 100644
--- a/core/binary_protocol/src/requests/system/get_snapshot.rs
+++ b/core/binary_protocol/src/requests/system/get_snapshot.rs
@@ -16,24 +16,57 @@
 // under the License.
 
 use crate::WireError;
-use crate::codec::{WireDecode, WireEncode};
-use bytes::BytesMut;
+use crate::codec::{WireDecode, WireEncode, read_u8};
+use bytes::{BufMut, BytesMut};
 
-/// `GetSnapshot` request. Wire format: empty.
+/// `GetSnapshot` request.
+///
+/// Wire format:
+/// ```text
+/// [compression:1][types_count:1][snapshot_type:1]*
+/// ```
 #[derive(Debug, Clone, PartialEq, Eq)]
-pub struct GetSnapshotRequest;
+pub struct GetSnapshotRequest {
+    /// Compression method code (1=Stored, 2=Deflated, 3=Bzip2, 4=Zstd, 5=Lzma, 6=Xz).
+    pub compression: u8,
+    /// Snapshot type codes (1=FilesystemOverview, 2=ProcessList, 3=ResourceUsage,
+    /// 4=Test, 5=ServerLogs, 6=ServerConfig, 100=All).
+    pub snapshot_types: Vec<u8>,
+}
 
 impl WireEncode for GetSnapshotRequest {
     fn encoded_size(&self) -> usize {
-        0
+        1 + 1 + self.snapshot_types.len()
     }
 
-    fn encode(&self, _buf: &mut BytesMut) {}
+    fn encode(&self, buf: &mut BytesMut) {
+        buf.put_u8(self.compression);
+        let count =
+            u8::try_from(self.snapshot_types.len()).expect("snapshot_types count exceeds u8::MAX");
+        buf.put_u8(count);
+        for &code in &self.snapshot_types {
+            buf.put_u8(code);
+        }
+    }
 }
 
 impl WireDecode for GetSnapshotRequest {
-    fn decode(_buf: &[u8]) -> Result<(Self, usize), WireError> {
-        Ok((Self, 0))
+    fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
+        let compression = read_u8(buf, 0)?;
+        let types_count = read_u8(buf, 1)? as usize;
+        let mut pos = 2;
+        let mut snapshot_types = Vec::with_capacity(types_count);
+        for _ in 0..types_count {
+            snapshot_types.push(read_u8(buf, pos)?);
+            pos += 1;
+        }
+        Ok((
+            Self {
+                compression,
+                snapshot_types,
+            },
+            pos,
+        ))
     }
 }
 
@@ -43,11 +76,54 @@ mod tests {
 
     #[test]
     fn roundtrip() {
-        let req = GetSnapshotRequest;
+        let req = GetSnapshotRequest {
+            compression: 2,
+            snapshot_types: vec![1, 5],
+        };
+        let bytes = req.to_bytes();
+        assert_eq!(bytes.len(), 4); // 1 + 1 + 2
+        let (decoded, consumed) = GetSnapshotRequest::decode(&bytes).unwrap();
+        assert_eq!(consumed, bytes.len());
+        assert_eq!(decoded, req);
+    }
+
+    #[test]
+    fn roundtrip_empty_types() {
+        let req = GetSnapshotRequest {
+            compression: 1,
+            snapshot_types: vec![],
+        };
+        let bytes = req.to_bytes();
+        assert_eq!(bytes.len(), 2);
+        let (decoded, consumed) = GetSnapshotRequest::decode(&bytes).unwrap();
+        assert_eq!(consumed, 2);
+        assert_eq!(decoded, req);
+    }
+
+    #[test]
+    fn roundtrip_all_type() {
+        let req = GetSnapshotRequest {
+            compression: 2,
+            snapshot_types: vec![100],
+        };
         let bytes = req.to_bytes();
-        assert!(bytes.is_empty());
         let (decoded, consumed) = GetSnapshotRequest::decode(&bytes).unwrap();
-        assert_eq!(consumed, 0);
+        assert_eq!(consumed, bytes.len());
         assert_eq!(decoded, req);
     }
+
+    #[test]
+    fn truncated_returns_error() {
+        let req = GetSnapshotRequest {
+            compression: 2,
+            snapshot_types: vec![1, 2, 3],
+        };
+        let bytes = req.to_bytes();
+        for i in 0..bytes.len() {
+            assert!(
+                GetSnapshotRequest::decode(&bytes[..i]).is_err(),
+                "expected error for truncation at byte {i}"
+            );
+        }
+    }
 }
diff --git a/core/binary_protocol/src/requests/users/create_user.rs b/core/binary_protocol/src/requests/users/create_user.rs
index 632e077d9..8f17710c7 100644
--- a/core/binary_protocol/src/requests/users/create_user.rs
+++ b/core/binary_protocol/src/requests/users/create_user.rs
@@ -42,8 +42,10 @@ impl WireEncode for CreateUserRequest {
             + self.password.len()
             + 1 // status
             + 1 // has_permissions
-            + 4 // permissions_len (always present)
-            + self.permissions.as_ref().map_or(0, WireEncode::encoded_size)
+            + self
+                .permissions
+                .as_ref()
+                .map_or(0, |p| 4 + p.encoded_size())
     }
 
     fn encode(&self, buf: &mut BytesMut) {
@@ -60,7 +62,6 @@ impl WireEncode for CreateUserRequest {
             buf.put_slice(&perm_bytes);
         } else {
             buf.put_u8(0);
-            buf.put_u32_le(0);
         }
     }
 }
@@ -80,10 +81,9 @@ impl WireDecode for CreateUserRequest {
         let has_permissions = read_u8(buf, pos)?;
         pos += 1;
 
-        let perm_len = read_u32_le(buf, pos)? as usize;
-        pos += 4;
-
-        let permissions = if has_permissions == 1 && perm_len > 0 {
+        let permissions = if has_permissions == 1 {
+            let perm_len = read_u32_le(buf, pos)? as usize;
+            pos += 4;
             let (perms, consumed) = WirePermissions::decode(&buf[pos..])?;
             if consumed != perm_len {
                 return Err(WireError::Validation(Cow::Owned(format!(
@@ -197,8 +197,8 @@ mod tests {
         };
         let bytes = req.to_bytes();
         // username: [1, b'u'] + password: [1, b'p'] + status: [0]
-        // + has_perm: [0] + perm_len: [0,0,0,0]
-        let expected: &[u8] = &[1, b'u', 1, b'p', 0, 0, 0, 0, 0, 0];
+        // + has_perm: [0]
+        let expected: &[u8] = &[1, b'u', 1, b'p', 0, 0];
         assert_eq!(&bytes[..], expected);
     }
 }
diff --git a/core/binary_protocol/src/requests/users/update_permissions.rs b/core/binary_protocol/src/requests/users/update_permissions.rs
index 16de85737..c304e1a15 100644
--- a/core/binary_protocol/src/requests/users/update_permissions.rs
+++ b/core/binary_protocol/src/requests/users/update_permissions.rs
@@ -36,8 +36,10 @@ impl WireEncode for UpdatePermissionsRequest {
     fn encoded_size(&self) -> usize {
         self.user_id.encoded_size()
             + 1 // has_permissions
-            + 4 // permissions_len (always present)
-            + self.permissions.as_ref().map_or(0, WireEncode::encoded_size)
+            + self
+                .permissions
+                .as_ref()
+                .map_or(0, |p| 4 + p.encoded_size())
     }
 
     fn encode(&self, buf: &mut BytesMut) {
@@ -50,7 +52,6 @@ impl WireEncode for UpdatePermissionsRequest {
             buf.put_slice(&perm_bytes);
         } else {
             buf.put_u8(0);
-            buf.put_u32_le(0);
         }
     }
 }
@@ -62,10 +63,9 @@ impl WireDecode for UpdatePermissionsRequest {
         let has_permissions = read_u8(buf, pos)?;
         pos += 1;
 
-        let perm_len = read_u32_le(buf, pos)? as usize;
-        pos += 4;
-
-        let permissions = if has_permissions == 1 && perm_len > 0 {
+        let permissions = if has_permissions == 1 {
+            let perm_len = read_u32_le(buf, pos)? as usize;
+            pos += 4;
             let (perms, consumed) = WirePermissions::decode(&buf[pos..])?;
             if consumed != perm_len {
                 return Err(WireError::Validation(Cow::Owned(format!(
diff --git a/core/integration/Cargo.toml b/core/integration/Cargo.toml
index 9851e5be0..aef357632 100644
--- a/core/integration/Cargo.toml
+++ b/core/integration/Cargo.toml
@@ -42,6 +42,7 @@ harness_derive = { workspace = true }
 humantime = { workspace = true }
 iggy = { workspace = true }
 iggy-cli = { workspace = true }
+iggy_binary_protocol = { workspace = true }
 iggy_common = { workspace = true }
 iggy_connector_sdk = { workspace = true, features = ["api"] }
 keyring = { workspace = true }
@@ -68,7 +69,6 @@ serial_test = { workspace = true }
 server = { workspace = true }
 socket2 = { workspace = true }
 sqlx = { workspace = true }
-strum = { workspace = true }
 sysinfo = { workspace = true }
 tempfile = { workspace = true }
 test-case = { workspace = true }
diff --git a/core/integration/tests/server/scenarios/authentication_scenario.rs b/core/integration/tests/server/scenarios/authentication_scenario.rs
index 4dc440c4f..6e476cf4c 100644
--- a/core/integration/tests/server/scenarios/authentication_scenario.rs
+++ b/core/integration/tests/server/scenarios/authentication_scenario.rs
@@ -23,15 +23,14 @@
 //! - LoginUser (authenticate with username/password)
 //! - LoginWithPersonalAccessToken (authenticate with PAT)
 //!
-//! Uses exhaustive matching on `ServerCommand` to ensure no command is missed
-//! when new commands are added.
+//! Covers all command codes from the dispatch table to ensure no command
+//! is missed when new commands are added.
 
 use crate::server::scenarios::create_client;
 use bytes::Bytes;
 use iggy::prelude::*;
+use iggy_binary_protocol::dispatch::COMMAND_TABLE;
 use integration::harness::{TestHarness, login_root};
-use server::binary::command::ServerCommand;
-use strum::IntoEnumIterator;
 
 const STREAM_NAME: &str = "auth-test-stream";
 const TOPIC_NAME: &str = "auth-test-topic";
@@ -103,142 +102,101 @@ pub async fn run(harness: &TestHarness) {
     cleanup_test_resources(&client).await;
 }
 
-/// Tests all commands require authentication using exhaustive matching.
-/// If a new command is added to ServerCommand, this match becomes non-exhaustive.
+/// Tests all commands require authentication by iterating the dispatch table.
+/// New entries in `COMMAND_TABLE` will hit the wildcard arm and panic,
+/// forcing an explicit decision about each new command.
 async fn test_all_commands_require_auth(client: &IggyClient) {
+    use iggy_binary_protocol::codes::*;
+
     let ctx = TestContext::new();
 
-    for cmd in ServerCommand::iter() {
-        let (name, result): (&str, Result<(), IggyError>) = match cmd {
-            // ================================================================
-            // NO AUTH REQUIRED (3 commands)
-            // ================================================================
-            ServerCommand::Ping(_)
-            | ServerCommand::LoginUser(_)
-            | ServerCommand::LoginWithPersonalAccessToken(_) => continue,
-
-            // ================================================================
-            // STATEFUL - NOT SUPPORTED ON HTTP (2 commands)
-            // ================================================================
-            ServerCommand::JoinConsumerGroup(_) | ServerCommand::LeaveConsumerGroup(_) => continue,
-
-            // ================================================================
-            // SPECIAL SETUP REQUIRED (3 commands)
-            // ================================================================
-            ServerCommand::GetSnapshot(_)
-            | ServerCommand::DeleteSegments(_)
-            | ServerCommand::LogoutUser(_) => continue,
-
-            // ================================================================
-            // REQUIRES AUTH (39 commands)
-            // ================================================================
+    for entry in COMMAND_TABLE {
+        let code = entry.code;
+        let name = entry.name;
+
+        // ================================================================
+        // SKIPPED COMMANDS (8 total)
+        // ================================================================
+        // No auth required
+        if matches!(
+            code,
+            PING_CODE | LOGIN_USER_CODE | LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE
+        ) {
+            continue;
+        }
+        // Stateful - not supported on HTTP
+        if matches!(code, JOIN_CONSUMER_GROUP_CODE | LEAVE_CONSUMER_GROUP_CODE) {
+            continue;
+        }
+        // Special setup required
+        if matches!(
+            code,
+            GET_SNAPSHOT_FILE_CODE | DELETE_SEGMENTS_CODE | LOGOUT_USER_CODE
+        ) {
+            continue;
+        }
 
+        // ================================================================
+        // REQUIRES AUTH
+        // ================================================================
+        let result: Result<(), IggyError> = match code {
             // System
-            ServerCommand::GetStats(_) => ("GetStats", client.get_stats().await.map(|_| ())),
-            ServerCommand::GetMe(_) => ("GetMe", client.get_me().await.map(|_| ())),
-            ServerCommand::GetClient(_) => ("GetClient", client.get_client(1).await.map(|_| ())),
-            ServerCommand::GetClients(_) => ("GetClients", client.get_clients().await.map(|_| ())),
-            ServerCommand::GetClusterMetadata(_) => (
-                "GetClusterMetadata",
-                client.get_cluster_metadata().await.map(|_| ()),
-            ),
+            GET_STATS_CODE => client.get_stats().await.map(|_| ()),
+            GET_ME_CODE => client.get_me().await.map(|_| ()),
+            GET_CLIENT_CODE => client.get_client(1).await.map(|_| ()),
+            GET_CLIENTS_CODE => client.get_clients().await.map(|_| ()),
+            GET_CLUSTER_METADATA_CODE => client.get_cluster_metadata().await.map(|_| ()),
 
             // Users
-            ServerCommand::GetUser(_) => {
-                ("GetUser", client.get_user(&ctx.user_id).await.map(|_| ()))
-            }
-            ServerCommand::GetUsers(_) => ("GetUsers", client.get_users().await.map(|_| ())),
-            ServerCommand::CreateUser(_) => (
-                "CreateUser",
-                client
-                    .create_user("test", "test", UserStatus::Active, None)
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::DeleteUser(_) => ("DeleteUser", client.delete_user(&ctx.user_id).await),
-            ServerCommand::UpdateUser(_) => (
-                "UpdateUser",
-                client.update_user(&ctx.user_id, Some("x"), None).await,
-            ),
-            ServerCommand::UpdatePermissions(_) => (
-                "UpdatePermissions",
-                client.update_permissions(&ctx.user_id, None).await,
-            ),
-            ServerCommand::ChangePassword(_) => (
-                "ChangePassword",
-                client.change_password(&ctx.user_id, "old", "new").await,
-            ),
+            GET_USER_CODE => client.get_user(&ctx.user_id).await.map(|_| ()),
+            GET_USERS_CODE => client.get_users().await.map(|_| ()),
+            CREATE_USER_CODE => client
+                .create_user("test", "test", UserStatus::Active, None)
+                .await
+                .map(|_| ()),
+            DELETE_USER_CODE => client.delete_user(&ctx.user_id).await,
+            UPDATE_USER_CODE => client.update_user(&ctx.user_id, Some("x"), None).await,
+            UPDATE_PERMISSIONS_CODE => client.update_permissions(&ctx.user_id, None).await,
+            CHANGE_PASSWORD_CODE => client.change_password(&ctx.user_id, "old", "new").await,
 
             // PAT
-            ServerCommand::GetPersonalAccessTokens(_) => (
-                "GetPersonalAccessTokens",
-                client.get_personal_access_tokens().await.map(|_| ()),
-            ),
-            ServerCommand::CreatePersonalAccessToken(_) => (
-                "CreatePersonalAccessToken",
-                client
-                    .create_personal_access_token("x", PersonalAccessTokenExpiry::NeverExpire)
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::DeletePersonalAccessToken(_) => (
-                "DeletePersonalAccessToken",
-                client.delete_personal_access_token("x").await,
-            ),
+            GET_PERSONAL_ACCESS_TOKENS_CODE => {
+                client.get_personal_access_tokens().await.map(|_| ())
+            }
+            CREATE_PERSONAL_ACCESS_TOKEN_CODE => client
+                .create_personal_access_token("x", PersonalAccessTokenExpiry::NeverExpire)
+                .await
+                .map(|_| ()),
+            DELETE_PERSONAL_ACCESS_TOKEN_CODE => client.delete_personal_access_token("x").await,
 
             // Streams
-            ServerCommand::GetStream(_) => (
-                "GetStream",
-                client.get_stream(&ctx.stream_id).await.map(|_| ()),
-            ),
-            ServerCommand::GetStreams(_) => ("GetStreams", client.get_streams().await.map(|_| ())),
-            ServerCommand::CreateStream(_) => {
-                ("CreateStream", client.create_stream("x").await.map(|_| ()))
-            }
-            ServerCommand::DeleteStream(_) => {
-                ("DeleteStream", client.delete_stream(&ctx.stream_id).await)
-            }
-            ServerCommand::UpdateStream(_) => (
-                "UpdateStream",
-                client.update_stream(&ctx.stream_id, "x").await,
-            ),
-            ServerCommand::PurgeStream(_) => {
-                ("PurgeStream", client.purge_stream(&ctx.stream_id).await)
-            }
+            GET_STREAM_CODE => client.get_stream(&ctx.stream_id).await.map(|_| ()),
+            GET_STREAMS_CODE => client.get_streams().await.map(|_| ()),
+            CREATE_STREAM_CODE => client.create_stream("x").await.map(|_| ()),
+            DELETE_STREAM_CODE => client.delete_stream(&ctx.stream_id).await,
+            UPDATE_STREAM_CODE => client.update_stream(&ctx.stream_id, "x").await,
+            PURGE_STREAM_CODE => client.purge_stream(&ctx.stream_id).await,
 
             // Topics
-            ServerCommand::GetTopic(_) => (
-                "GetTopic",
-                client
-                    .get_topic(&ctx.stream_id, &ctx.topic_id)
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::GetTopics(_) => (
-                "GetTopics",
-                client.get_topics(&ctx.stream_id).await.map(|_| ()),
-            ),
-            ServerCommand::CreateTopic(_) => (
-                "CreateTopic",
-                client
-                    .create_topic(
-                        &ctx.stream_id,
-                        "x",
-                        1,
-                        CompressionAlgorithm::None,
-                        None,
-                        IggyExpiry::NeverExpire,
-                        MaxTopicSize::ServerDefault,
-                    )
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::DeleteTopic(_) => (
-                "DeleteTopic",
-                client.delete_topic(&ctx.stream_id, &ctx.topic_id).await,
-            ),
-            ServerCommand::UpdateTopic(_) => (
-                "UpdateTopic",
+            GET_TOPIC_CODE => client
+                .get_topic(&ctx.stream_id, &ctx.topic_id)
+                .await
+                .map(|_| ()),
+            GET_TOPICS_CODE => client.get_topics(&ctx.stream_id).await.map(|_| ()),
+            CREATE_TOPIC_CODE => client
+                .create_topic(
+                    &ctx.stream_id,
+                    "x",
+                    1,
+                    CompressionAlgorithm::None,
+                    None,
+                    IggyExpiry::NeverExpire,
+                    MaxTopicSize::ServerDefault,
+                )
+                .await
+                .map(|_| ()),
+            DELETE_TOPIC_CODE => client.delete_topic(&ctx.stream_id, &ctx.topic_id).await,
+            UPDATE_TOPIC_CODE => {
                 client
                     .update_topic(
                         &ctx.stream_id,
@@ -249,118 +207,93 @@ async fn test_all_commands_require_auth(client: &IggyClient) {
                         IggyExpiry::NeverExpire,
                         MaxTopicSize::ServerDefault,
                     )
-                    .await,
-            ),
-            ServerCommand::PurgeTopic(_) => (
-                "PurgeTopic",
-                client.purge_topic(&ctx.stream_id, &ctx.topic_id).await,
-            ),
+                    .await
+            }
+            PURGE_TOPIC_CODE => client.purge_topic(&ctx.stream_id, &ctx.topic_id).await,
 
             // Partitions
-            ServerCommand::CreatePartitions(_) => (
-                "CreatePartitions",
+            CREATE_PARTITIONS_CODE => {
                 client
                     .create_partitions(&ctx.stream_id, &ctx.topic_id, 1)
-                    .await,
-            ),
-            ServerCommand::DeletePartitions(_) => (
-                "DeletePartitions",
+                    .await
+            }
+            DELETE_PARTITIONS_CODE => {
                 client
                     .delete_partitions(&ctx.stream_id, &ctx.topic_id, 1)
-                    .await,
-            ),
+                    .await
+            }
 
             // Messages
-            ServerCommand::SendMessages(_) => {
+            SEND_MESSAGES_CODE => {
                 let mut msgs = vec![
                     IggyMessage::builder()
                         .payload(Bytes::from("x"))
                         .build()
                         .unwrap(),
                 ];
-                (
-                    "SendMessages",
-                    client
-                        .send_messages(
-                            &ctx.stream_id,
-                            &ctx.topic_id,
-                            &Partitioning::partition_id(0),
-                            &mut msgs,
-                        )
-                        .await,
-                )
-            }
-            ServerCommand::PollMessages(_) => (
-                "PollMessages",
                 client
-                    .poll_messages(
+                    .send_messages(
                         &ctx.stream_id,
                         &ctx.topic_id,
-                        Some(0),
-                        &ctx.consumer,
-                        &PollingStrategy::offset(0),
-                        1,
-                        false,
+                        &Partitioning::partition_id(0),
+                        &mut msgs,
                     )
                     .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::FlushUnsavedBuffer(_) => (
-                "FlushUnsavedBuffer",
+            }
+            POLL_MESSAGES_CODE => client
+                .poll_messages(
+                    &ctx.stream_id,
+                    &ctx.topic_id,
+                    Some(0),
+                    &ctx.consumer,
+                    &PollingStrategy::offset(0),
+                    1,
+                    false,
+                )
+                .await
+                .map(|_| ()),
+            FLUSH_UNSAVED_BUFFER_CODE => {
                 client
                     .flush_unsaved_buffer(&ctx.stream_id, &ctx.topic_id, 0, false)
-                    .await,
-            ),
+                    .await
+            }
 
             // Consumer Offsets
-            ServerCommand::GetConsumerOffset(_) => (
-                "GetConsumerOffset",
-                client
-                    .get_consumer_offset(&ctx.consumer, &ctx.stream_id, &ctx.topic_id, Some(0))
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::StoreConsumerOffset(_) => (
-                "StoreConsumerOffset",
+            GET_CONSUMER_OFFSET_CODE => client
+                .get_consumer_offset(&ctx.consumer, &ctx.stream_id, &ctx.topic_id, Some(0))
+                .await
+                .map(|_| ()),
+            STORE_CONSUMER_OFFSET_CODE => {
                 client
                     .store_consumer_offset(&ctx.consumer, &ctx.stream_id, &ctx.topic_id, Some(0), 0)
-                    .await,
-            ),
-            ServerCommand::DeleteConsumerOffset(_) => (
-                "DeleteConsumerOffset",
+                    .await
+            }
+            DELETE_CONSUMER_OFFSET_CODE => {
                 client
                     .delete_consumer_offset(&ctx.consumer, &ctx.stream_id, &ctx.topic_id, Some(0))
-                    .await,
-            ),
+                    .await
+            }
 
             // Consumer Groups
-            ServerCommand::GetConsumerGroup(_) => (
-                "GetConsumerGroup",
-                client
-                    .get_consumer_group(&ctx.stream_id, &ctx.topic_id, &ctx.group_id)
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::GetConsumerGroups(_) => (
-                "GetConsumerGroups",
-                client
-                    .get_consumer_groups(&ctx.stream_id, &ctx.topic_id)
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::CreateConsumerGroup(_) => (
-                "CreateConsumerGroup",
-                client
-                    .create_consumer_group(&ctx.stream_id, &ctx.topic_id, "x")
-                    .await
-                    .map(|_| ()),
-            ),
-            ServerCommand::DeleteConsumerGroup(_) => (
-                "DeleteConsumerGroup",
+            GET_CONSUMER_GROUP_CODE => client
+                .get_consumer_group(&ctx.stream_id, &ctx.topic_id, &ctx.group_id)
+                .await
+                .map(|_| ()),
+            GET_CONSUMER_GROUPS_CODE => client
+                .get_consumer_groups(&ctx.stream_id, &ctx.topic_id)
+                .await
+                .map(|_| ()),
+            CREATE_CONSUMER_GROUP_CODE => client
+                .create_consumer_group(&ctx.stream_id, &ctx.topic_id, "x")
+                .await
+                .map(|_| ()),
+            DELETE_CONSUMER_GROUP_CODE => {
                 client
                     .delete_consumer_group(&ctx.stream_id, &ctx.topic_id, &ctx.group_id)
-                    .await,
-            ),
+                    .await
+            }
+
+            _ => panic!("Unhandled command code {code} ({name}) in auth test"),
         };
 
         assert_unauthenticated(result, name);
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index c852010f3..62027ca6c 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -55,7 +55,6 @@ cyper = { workspace = true }
 cyper-axum = { workspace = true }
 dashmap = { workspace = true }
 dotenvy = { workspace = true }
-enum_dispatch = { workspace = true }
 err_trail = { workspace = true }
 error_set = { workspace = true }
 figlet-rs = { workspace = true }
@@ -64,6 +63,7 @@ fs2 = { workspace = true }
 futures = { workspace = true }
 hash32 = { workspace = true }
 human-repr = { workspace = true }
+iggy_binary_protocol = { workspace = true }
 iggy_common = { workspace = true }
 jsonwebtoken = { workspace = true }
 left-right = { workspace = true }
diff --git a/core/server/src/binary/command.rs b/core/server/src/binary/command.rs
deleted file mode 100644
index cac319f59..000000000
--- a/core/server/src/binary/command.rs
+++ /dev/null
@@ -1,432 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::define_server_command_enum;
-use crate::shard::IggyShard;
-use crate::streaming::session::Session;
-use bytes::{BufMut, Bytes, BytesMut};
-use enum_dispatch::enum_dispatch;
-use iggy_common::SenderKind;
-use iggy_common::change_password::ChangePassword;
-use iggy_common::create_consumer_group::CreateConsumerGroup;
-use iggy_common::create_partitions::CreatePartitions;
-use iggy_common::create_personal_access_token::CreatePersonalAccessToken;
-use iggy_common::create_stream::CreateStream;
-use iggy_common::create_topic::CreateTopic;
-use iggy_common::create_user::CreateUser;
-use iggy_common::delete_consumer_group::DeleteConsumerGroup;
-use iggy_common::delete_consumer_offset::DeleteConsumerOffset;
-use iggy_common::delete_partitions::DeletePartitions;
-use iggy_common::delete_personal_access_token::DeletePersonalAccessToken;
-use iggy_common::delete_segments::DeleteSegments;
-use iggy_common::delete_stream::DeleteStream;
-use iggy_common::delete_topic::DeleteTopic;
-use iggy_common::delete_user::DeleteUser;
-use iggy_common::get_client::GetClient;
-use iggy_common::get_clients::GetClients;
-use iggy_common::get_cluster_metadata::GetClusterMetadata;
-use iggy_common::get_consumer_group::GetConsumerGroup;
-use iggy_common::get_consumer_groups::GetConsumerGroups;
-use iggy_common::get_consumer_offset::GetConsumerOffset;
-use iggy_common::get_me::GetMe;
-use iggy_common::get_personal_access_tokens::GetPersonalAccessTokens;
-use iggy_common::get_snapshot::GetSnapshot;
-use iggy_common::get_stats::GetStats;
-use iggy_common::get_stream::GetStream;
-use iggy_common::get_streams::GetStreams;
-use iggy_common::get_topic::GetTopic;
-use iggy_common::get_topics::GetTopics;
-use iggy_common::get_user::GetUser;
-use iggy_common::get_users::GetUsers;
-use iggy_common::join_consumer_group::JoinConsumerGroup;
-use iggy_common::leave_consumer_group::LeaveConsumerGroup;
-use iggy_common::login_user::LoginUser;
-use iggy_common::login_with_personal_access_token::LoginWithPersonalAccessToken;
-use iggy_common::logout_user::LogoutUser;
-use iggy_common::ping::Ping;
-use iggy_common::purge_stream::PurgeStream;
-use iggy_common::purge_topic::PurgeTopic;
-use iggy_common::store_consumer_offset::StoreConsumerOffset;
-use iggy_common::update_permissions::UpdatePermissions;
-use iggy_common::update_stream::UpdateStream;
-use iggy_common::update_topic::UpdateTopic;
-use iggy_common::update_user::UpdateUser;
-use iggy_common::*;
-use std::rc::Rc;
-use strum::{EnumIter, EnumString};
-use tracing::error;
-
-define_server_command_enum! {
-    Ping(Ping), PING_CODE, PING, false;
-    GetStats(GetStats), GET_STATS_CODE, GET_STATS, false;
-    GetMe(GetMe), GET_ME_CODE, GET_ME, false;
-    GetClient(GetClient), GET_CLIENT_CODE, GET_CLIENT, true;
-    GetClients(GetClients), GET_CLIENTS_CODE, GET_CLIENTS, false;
-    GetSnapshot(GetSnapshot), GET_SNAPSHOT_FILE_CODE, GET_SNAPSHOT_FILE, false;
-    GetClusterMetadata(GetClusterMetadata), GET_CLUSTER_METADATA_CODE, GET_CLUSTER_METADATA, false;
-    PollMessages(PollMessages), POLL_MESSAGES_CODE, POLL_MESSAGES, true;
-    FlushUnsavedBuffer(FlushUnsavedBuffer), FLUSH_UNSAVED_BUFFER_CODE, FLUSH_UNSAVED_BUFFER, true;
-    GetUser(GetUser), GET_USER_CODE, GET_USER, true;
-    GetUsers(GetUsers), GET_USERS_CODE, GET_USERS, false;
-    CreateUser(CreateUser), CREATE_USER_CODE, CREATE_USER, true;
-    DeleteUser(DeleteUser), DELETE_USER_CODE, DELETE_USER, true;
-    UpdateUser(UpdateUser), UPDATE_USER_CODE, UPDATE_USER, true;
-    UpdatePermissions(UpdatePermissions), UPDATE_PERMISSIONS_CODE, UPDATE_PERMISSIONS, true;
-    ChangePassword(ChangePassword), CHANGE_PASSWORD_CODE, CHANGE_PASSWORD, true;
-    LoginUser(LoginUser), LOGIN_USER_CODE, LOGIN_USER, true;
-    LogoutUser(LogoutUser), LOGOUT_USER_CODE, LOGOUT_USER, false;
-    GetPersonalAccessTokens(GetPersonalAccessTokens), GET_PERSONAL_ACCESS_TOKENS_CODE, GET_PERSONAL_ACCESS_TOKENS, false;
-    CreatePersonalAccessToken(CreatePersonalAccessToken), CREATE_PERSONAL_ACCESS_TOKEN_CODE, CREATE_PERSONAL_ACCESS_TOKEN, true;
-    DeletePersonalAccessToken(DeletePersonalAccessToken), DELETE_PERSONAL_ACCESS_TOKEN_CODE, DELETE_PERSONAL_ACCESS_TOKEN, false;
-    LoginWithPersonalAccessToken(LoginWithPersonalAccessToken), LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE, LOGIN_WITH_PERSONAL_ACCESS_TOKEN, true;
-    SendMessages(SendMessages), SEND_MESSAGES_CODE, SEND_MESSAGES, false;
-    GetConsumerOffset(GetConsumerOffset), GET_CONSUMER_OFFSET_CODE, GET_CONSUMER_OFFSET, true;
-    StoreConsumerOffset(StoreConsumerOffset), STORE_CONSUMER_OFFSET_CODE, STORE_CONSUMER_OFFSET, true;
-    DeleteConsumerOffset(DeleteConsumerOffset), DELETE_CONSUMER_OFFSET_CODE, DELETE_CONSUMER_OFFSET, true;
-    GetStream(GetStream), GET_STREAM_CODE, GET_STREAM, true;
-    GetStreams(GetStreams), GET_STREAMS_CODE, GET_STREAMS, false;
-    CreateStream(CreateStream), CREATE_STREAM_CODE, CREATE_STREAM, true;
-    DeleteStream(DeleteStream), DELETE_STREAM_CODE, DELETE_STREAM, true;
-    UpdateStream(UpdateStream), UPDATE_STREAM_CODE, UPDATE_STREAM, true;
-    PurgeStream(PurgeStream), PURGE_STREAM_CODE, PURGE_STREAM, true;
-    GetTopic(GetTopic), GET_TOPIC_CODE, GET_TOPIC, true;
-    GetTopics(GetTopics), GET_TOPICS_CODE, GET_TOPICS, false;
-    CreateTopic(CreateTopic), CREATE_TOPIC_CODE, CREATE_TOPIC, true;
-    DeleteTopic(DeleteTopic), DELETE_TOPIC_CODE, DELETE_TOPIC, true;
-    UpdateTopic(UpdateTopic), UPDATE_TOPIC_CODE, UPDATE_TOPIC, true;
-    PurgeTopic(PurgeTopic), PURGE_TOPIC_CODE, PURGE_TOPIC, true;
-    CreatePartitions(CreatePartitions), CREATE_PARTITIONS_CODE, CREATE_PARTITIONS, true;
-    DeletePartitions(DeletePartitions), DELETE_PARTITIONS_CODE, DELETE_PARTITIONS, true;
-    DeleteSegments(DeleteSegments), DELETE_SEGMENTS_CODE, DELETE_SEGMENTS, true;
-    GetConsumerGroup(GetConsumerGroup), GET_CONSUMER_GROUP_CODE, GET_CONSUMER_GROUP, true;
-    GetConsumerGroups(GetConsumerGroups), GET_CONSUMER_GROUPS_CODE, GET_CONSUMER_GROUPS, false;
-    CreateConsumerGroup(CreateConsumerGroup), CREATE_CONSUMER_GROUP_CODE, CREATE_CONSUMER_GROUP, true;
-    DeleteConsumerGroup(DeleteConsumerGroup), DELETE_CONSUMER_GROUP_CODE, DELETE_CONSUMER_GROUP, true;
-    JoinConsumerGroup(JoinConsumerGroup), JOIN_CONSUMER_GROUP_CODE, JOIN_CONSUMER_GROUP, true;
-    LeaveConsumerGroup(LeaveConsumerGroup), LEAVE_CONSUMER_GROUP_CODE, LEAVE_CONSUMER_GROUP, true;
-}
-
-/// Indicates whether a command handler completed normally or migrated the connection.
-pub enum HandlerResult {
-    /// Command completed, connection stays on current shard.
-    Finished,
-
-    /// Connection was migrated to another shard. Source shard should exit without cleanup.
-    Migrated { to_shard: u16 },
-}
-
-#[enum_dispatch]
-pub trait ServerCommandHandler {
-    /// Return the command code
-    fn code(&self) -> u32;
-
-    /// Handle the command execution
-    #[allow(async_fn_in_trait)]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError>;
-}
-
-pub trait BinaryServerCommand {
-    /// Parse command from sender
-    #[allow(async_fn_in_trait)]
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError>
-    where
-        Self: Sized;
-}
-
-fn as_bytes<T: Command>(command: &T) -> Bytes {
-    let payload = command.to_bytes();
-    let mut bytes = BytesMut::with_capacity(4 + payload.len());
-    bytes.put_u32_le(command.code());
-    bytes.put_slice(&payload);
-    bytes.freeze()
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn should_be_serialized_as_bytes_and_deserialized_from_bytes() {
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::Ping(Ping::default()),
-            PING_CODE,
-            &Ping::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetStats(GetStats::default()),
-            GET_STATS_CODE,
-            &GetStats::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetClusterMetadata(GetClusterMetadata::default()),
-            GET_CLUSTER_METADATA_CODE,
-            &GetClusterMetadata::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetMe(GetMe::default()),
-            GET_ME_CODE,
-            &GetMe::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetClient(GetClient::default()),
-            GET_CLIENT_CODE,
-            &GetClient::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetClients(GetClients::default()),
-            GET_CLIENTS_CODE,
-            &GetClients::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetUser(GetUser::default()),
-            GET_USER_CODE,
-            &GetUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetUsers(GetUsers::default()),
-            GET_USERS_CODE,
-            &GetUsers::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreateUser(CreateUser::default()),
-            CREATE_USER_CODE,
-            &CreateUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeleteUser(DeleteUser::default()),
-            DELETE_USER_CODE,
-            &DeleteUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::UpdateUser(UpdateUser::default()),
-            UPDATE_USER_CODE,
-            &UpdateUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::UpdatePermissions(UpdatePermissions::default()),
-            UPDATE_PERMISSIONS_CODE,
-            &UpdatePermissions::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::ChangePassword(ChangePassword::default()),
-            CHANGE_PASSWORD_CODE,
-            &ChangePassword::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::LoginUser(LoginUser::default()),
-            LOGIN_USER_CODE,
-            &LoginUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::LogoutUser(LogoutUser::default()),
-            LOGOUT_USER_CODE,
-            &LogoutUser::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetPersonalAccessTokens(GetPersonalAccessTokens::default()),
-            GET_PERSONAL_ACCESS_TOKENS_CODE,
-            &GetPersonalAccessTokens::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreatePersonalAccessToken(CreatePersonalAccessToken::default()),
-            CREATE_PERSONAL_ACCESS_TOKEN_CODE,
-            &CreatePersonalAccessToken::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeletePersonalAccessToken(DeletePersonalAccessToken::default()),
-            DELETE_PERSONAL_ACCESS_TOKEN_CODE,
-            &DeletePersonalAccessToken::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::LoginWithPersonalAccessToken(LoginWithPersonalAccessToken::default()),
-            LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE,
-            &LoginWithPersonalAccessToken::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::PollMessages(PollMessages::default()),
-            POLL_MESSAGES_CODE,
-            &PollMessages::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::StoreConsumerOffset(StoreConsumerOffset::default()),
-            STORE_CONSUMER_OFFSET_CODE,
-            &StoreConsumerOffset::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetConsumerOffset(GetConsumerOffset::default()),
-            GET_CONSUMER_OFFSET_CODE,
-            &GetConsumerOffset::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetStream(GetStream::default()),
-            GET_STREAM_CODE,
-            &GetStream::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetStreams(GetStreams::default()),
-            GET_STREAMS_CODE,
-            &GetStreams::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreateStream(CreateStream::default()),
-            CREATE_STREAM_CODE,
-            &CreateStream::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeleteStream(DeleteStream::default()),
-            DELETE_STREAM_CODE,
-            &DeleteStream::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::UpdateStream(UpdateStream::default()),
-            UPDATE_STREAM_CODE,
-            &UpdateStream::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::PurgeStream(PurgeStream::default()),
-            PURGE_STREAM_CODE,
-            &PurgeStream::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetTopic(GetTopic::default()),
-            GET_TOPIC_CODE,
-            &GetTopic::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetTopics(GetTopics::default()),
-            GET_TOPICS_CODE,
-            &GetTopics::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreateTopic(CreateTopic::default()),
-            CREATE_TOPIC_CODE,
-            &CreateTopic::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeleteTopic(DeleteTopic::default()),
-            DELETE_TOPIC_CODE,
-            &DeleteTopic::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::UpdateTopic(UpdateTopic::default()),
-            UPDATE_TOPIC_CODE,
-            &UpdateTopic::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::PurgeTopic(PurgeTopic::default()),
-            PURGE_TOPIC_CODE,
-            &PurgeTopic::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreatePartitions(CreatePartitions::default()),
-            CREATE_PARTITIONS_CODE,
-            &CreatePartitions::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeletePartitions(DeletePartitions::default()),
-            DELETE_PARTITIONS_CODE,
-            &DeletePartitions::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeleteSegments(DeleteSegments::default()),
-            DELETE_SEGMENTS_CODE,
-            &DeleteSegments::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetConsumerGroup(GetConsumerGroup::default()),
-            GET_CONSUMER_GROUP_CODE,
-            &GetConsumerGroup::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::GetConsumerGroups(GetConsumerGroups::default()),
-            GET_CONSUMER_GROUPS_CODE,
-            &GetConsumerGroups::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::CreateConsumerGroup(CreateConsumerGroup::default()),
-            CREATE_CONSUMER_GROUP_CODE,
-            &CreateConsumerGroup::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::DeleteConsumerGroup(DeleteConsumerGroup::default()),
-            DELETE_CONSUMER_GROUP_CODE,
-            &DeleteConsumerGroup::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::JoinConsumerGroup(JoinConsumerGroup::default()),
-            JOIN_CONSUMER_GROUP_CODE,
-            &JoinConsumerGroup::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::LeaveConsumerGroup(LeaveConsumerGroup::default()),
-            LEAVE_CONSUMER_GROUP_CODE,
-            &LeaveConsumerGroup::default(),
-        );
-        assert_serialized_as_bytes_and_deserialized_from_bytes(
-            &ServerCommand::FlushUnsavedBuffer(FlushUnsavedBuffer::default()),
-            FLUSH_UNSAVED_BUFFER_CODE,
-            &FlushUnsavedBuffer::default(),
-        );
-    }
-
-    fn assert_serialized_as_bytes_and_deserialized_from_bytes(
-        command: &ServerCommand,
-        code: u32,
-        payload: &dyn Command,
-    ) {
-        assert_serialized_as_bytes(command, code, payload);
-        assert_deserialized_from_bytes(command, code, payload);
-    }
-
-    fn assert_serialized_as_bytes(
-        server_command: &ServerCommand,
-        code: u32,
-        command: &dyn Command,
-    ) {
-        let payload = command.to_bytes();
-        let mut bytes = BytesMut::with_capacity(4 + payload.len());
-        bytes.put_u32_le(code);
-        bytes.put_slice(&payload);
-        assert_eq!(server_command.to_bytes(), bytes);
-    }
-
-    fn assert_deserialized_from_bytes(
-        command: &ServerCommand,
-        command_id: u32,
-        payload: &dyn Command,
-    ) {
-        let payload = payload.to_bytes();
-        let mut bytes = BytesMut::with_capacity(payload.len());
-        bytes.put_slice(&payload);
-        let bytes = Bytes::from(bytes);
-        let deserialized = ServerCommand::from_code_and_payload(command_id, bytes).unwrap();
-        // SecretString doesn't implement PartialEq, so we compare serialized bytes instead.
-        // This is sufficient since to_bytes() covers all wire-protocol-relevant fields.
-        assert_eq!(deserialized.to_bytes(), command.to_bytes());
-    }
-}
diff --git a/core/server/src/binary/dispatch.rs b/core/server/src/binary/dispatch.rs
new file mode 100644
index 000000000..2ef985e1d
--- /dev/null
+++ b/core/server/src/binary/dispatch.rs
@@ -0,0 +1,581 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::binary::handlers;
+use crate::shard::IggyShard;
+use crate::streaming::session::Session;
+use ahash::AHashMap;
+use bytes::BytesMut;
+use iggy_binary_protocol::RequestFrame;
+use iggy_binary_protocol::codec::WireDecode;
+use iggy_binary_protocol::codes::*;
+use iggy_binary_protocol::primitives::permissions::{
+    WireGlobalPermissions, WirePermissions, WireStreamPermissions, WireTopicPermissions,
+};
+use iggy_binary_protocol::requests::consumer_groups::*;
+use iggy_binary_protocol::requests::consumer_offsets::*;
+use iggy_binary_protocol::requests::messages::*;
+use iggy_binary_protocol::requests::partitions::*;
+use iggy_binary_protocol::requests::personal_access_tokens::*;
+use iggy_binary_protocol::requests::segments::*;
+use iggy_binary_protocol::requests::streams::*;
+use iggy_binary_protocol::requests::system::*;
+use iggy_binary_protocol::requests::topics::*;
+use iggy_binary_protocol::requests::users::*;
+use iggy_common::{
+    Consumer, ConsumerKind, GlobalPermissions, Identifier, IggyError, Permissions, PollingKind,
+    PollingStrategy, SenderKind, StreamPermissions, TopicPermissions,
+};
+use std::rc::Rc;
+use tracing::{error, warn};
+
+/// Result of handling a command. Most commands return `Finished`.
+/// `SendMessages` may migrate the TCP connection to another shard.
+pub enum HandlerResult {
+    Finished,
+    Migrated { to_shard: u16 },
+}
+
+/// Read the full payload from the sender into a buffer.
+pub async fn read_payload(sender: &mut SenderKind, length: u32) -> Result<BytesMut, IggyError> {
+    if length > MAX_CONTROL_FRAME_PAYLOAD {
+        return Err(IggyError::InvalidCommand);
+    }
+    let mut buffer = BytesMut::with_capacity(length as usize);
+    // SAFETY: when length > 0, sender.read() fills exactly `length` bytes
+    // before returning Ok. On error the buffer is dropped without being read.
+    // When length == 0, set_len(0) is a no-op (no uninitialized bytes exposed).
+    unsafe {
+        buffer.set_len(length as usize);
+    }
+    if length > 0 {
+        let (result, buf) = sender.read(buffer).await;
+        result?;
+        buffer = buf;
+    }
+    Ok(buffer)
+}
+
+fn decode<T: WireDecode>(payload: &[u8]) -> Result<T, IggyError> {
+    use iggy_binary_protocol::error::WireError;
+
+    let (val, consumed) = T::decode(payload).map_err(|e| {
+        warn!("wire decode error: {e}");
+        match e {
+            WireError::PayloadTooLarge { .. } => IggyError::InvalidSizeBytes,
+            WireError::Validation(_) => IggyError::InvalidFormat,
+            _ => IggyError::InvalidCommand,
+        }
+    })?;
+    if consumed != payload.len() {
+        warn!(
+            "wire decode: {} trailing bytes (consumed {consumed}, payload {})",
+            payload.len() - consumed,
+            payload.len()
+        );
+    }
+    Ok(val)
+}
+
+/// Convert a `WireIdentifier` to the domain `Identifier`.
+pub fn wire_id_to_identifier(
+    wire: &iggy_binary_protocol::WireIdentifier,
+) -> Result<Identifier, IggyError> {
+    match wire {
+        iggy_binary_protocol::WireIdentifier::Numeric(id) => Identifier::numeric(*id),
+        iggy_binary_protocol::WireIdentifier::String(name) => Identifier::named(name.as_str()),
+    }
+}
+
+/// Convert a `WireConsumer` to the domain `Consumer`.
+pub fn wire_consumer_to_consumer(
+    wire: &iggy_binary_protocol::WireConsumer,
+) -> Result<Consumer, IggyError> {
+    let id = wire_id_to_identifier(&wire.id)?;
+    let kind = ConsumerKind::from_code(wire.kind)?;
+    Ok(Consumer { kind, id })
+}
+
+/// Convert a `WirePollingStrategy` to the domain `PollingStrategy`.
+pub fn wire_polling_to_strategy(
+    wire: &iggy_binary_protocol::WirePollingStrategy,
+) -> Result<PollingStrategy, IggyError> {
+    Ok(PollingStrategy {
+        kind: PollingKind::from_code(wire.kind)?,
+        value: wire.value,
+    })
+}
+
+fn wire_topic_to_domain(wt: &WireTopicPermissions) -> TopicPermissions {
+    TopicPermissions {
+        manage_topic: wt.manage_topic,
+        read_topic: wt.read_topic,
+        poll_messages: wt.poll_messages,
+        send_messages: wt.send_messages,
+    }
+}
+
+fn wire_stream_to_domain(ws: &WireStreamPermissions) -> StreamPermissions {
+    let topics = if ws.topics.is_empty() {
+        None
+    } else {
+        let mut map = AHashMap::with_capacity(ws.topics.len());
+        for wt in &ws.topics {
+            map.insert(wt.topic_id as usize, wire_topic_to_domain(wt));
+        }
+        Some(map)
+    };
+    StreamPermissions {
+        manage_stream: ws.manage_stream,
+        read_stream: ws.read_stream,
+        manage_topics: ws.manage_topics,
+        read_topics: ws.read_topics,
+        poll_messages: ws.poll_messages,
+        send_messages: ws.send_messages,
+        topics,
+    }
+}
+
+/// Convert `WirePermissions` to the domain `Permissions`.
+pub fn wire_permissions_to_permissions(wp: &WirePermissions) -> Permissions {
+    let streams = if wp.streams.is_empty() {
+        None
+    } else {
+        let mut map = AHashMap::with_capacity(wp.streams.len());
+        for ws in &wp.streams {
+            map.insert(ws.stream_id as usize, wire_stream_to_domain(ws));
+        }
+        Some(map)
+    };
+    Permissions {
+        global: GlobalPermissions {
+            manage_servers: wp.global.manage_servers,
+            read_servers: wp.global.read_servers,
+            manage_users: wp.global.manage_users,
+            read_users: wp.global.read_users,
+            manage_streams: wp.global.manage_streams,
+            read_streams: wp.global.read_streams,
+            manage_topics: wp.global.manage_topics,
+            read_topics: wp.global.read_topics,
+            poll_messages: wp.global.poll_messages,
+            send_messages: wp.global.send_messages,
+        },
+        streams,
+    }
+}
+
+fn domain_topic_to_wire(topic_id: usize, tp: &TopicPermissions) -> WireTopicPermissions {
+    WireTopicPermissions {
+        topic_id: topic_id as u32,
+        manage_topic: tp.manage_topic,
+        read_topic: tp.read_topic,
+        poll_messages: tp.poll_messages,
+        send_messages: tp.send_messages,
+    }
+}
+
+fn domain_stream_to_wire(stream_id: usize, sp: &StreamPermissions) -> WireStreamPermissions {
+    let topics: Vec<WireTopicPermissions> = sp
+        .topics
+        .as_ref()
+        .map(|map| {
+            map.iter()
+                .map(|(&tid, tp)| domain_topic_to_wire(tid, tp))
+                .collect()
+        })
+        .unwrap_or_default();
+    WireStreamPermissions {
+        stream_id: stream_id as u32,
+        manage_stream: sp.manage_stream,
+        read_stream: sp.read_stream,
+        manage_topics: sp.manage_topics,
+        read_topics: sp.read_topics,
+        poll_messages: sp.poll_messages,
+        send_messages: sp.send_messages,
+        topics,
+    }
+}
+
+/// Convert domain `Permissions` to `WirePermissions`.
+pub fn domain_permissions_to_wire(perms: &Permissions) -> WirePermissions {
+    let streams: Vec<WireStreamPermissions> = perms
+        .streams
+        .as_ref()
+        .map(|map| {
+            map.iter()
+                .map(|(&sid, sp)| domain_stream_to_wire(sid, sp))
+                .collect()
+        })
+        .unwrap_or_default();
+    WirePermissions {
+        global: WireGlobalPermissions {
+            manage_servers: perms.global.manage_servers,
+            read_servers: perms.global.read_servers,
+            manage_users: perms.global.manage_users,
+            read_users: perms.global.read_users,
+            manage_streams: perms.global.manage_streams,
+            read_streams: perms.global.read_streams,
+            manage_topics: perms.global.manage_topics,
+            read_topics: perms.global.read_topics,
+            poll_messages: perms.global.poll_messages,
+            send_messages: perms.global.send_messages,
+        },
+        streams,
+    }
+}
+
+/// Maximum payload size for control-plane commands (non-SendMessages).
+/// Prevents OOM from malicious clients sending `length = u32::MAX`.
+/// SendMessages has its own size validation via `total_payload_size` checks.
+pub const MAX_CONTROL_FRAME_PAYLOAD: u32 = 10 * 1024 * 1024; // 10 MB
+
+/// Dispatch a SendMessages command with staged socket reads (zero-copy path).
+///
+/// Called by transport layers when the command code is `SEND_MESSAGES_CODE`.
+/// The handler reads metadata, indexes, and messages directly from the socket
+/// into separate `PooledBuffer`s for zero-copy partition append.
+pub async fn dispatch_send_messages(
+    sender: &mut SenderKind,
+    payload_length: u32,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    handlers::messages::send_messages_handler::handle_send_messages(
+        sender,
+        payload_length,
+        session,
+        shard,
+    )
+    .await
+}
+
+/// Central command dispatch for a decoded request frame.
+///
+/// Transport layers read the 8-byte header, validate via
+/// `RequestFrame::payload_length()`, read the full payload, construct a
+/// `RequestFrame::from_parts(code, frame.payload)`, and pass it here.
+///
+/// SendMessages is handled separately via `dispatch_send_messages()`.
+#[allow(clippy::too_many_lines)]
+pub async fn dispatch(
+    frame: RequestFrame<'_>,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    match frame.code {
+        // System
+        PING_CODE => {
+            handlers::system::ping_handler::handle_ping(sender, session, shard).await
+        }
+        GET_STATS_CODE => {
+            handlers::system::get_stats_handler::handle_get_stats(sender, session, shard).await
+        }
+        GET_ME_CODE => {
+            handlers::system::get_me_handler::handle_get_me(sender, session, shard).await
+        }
+        GET_CLIENT_CODE => {
+            let req: GetClientRequest = decode(frame.payload)?;
+            handlers::system::get_client_handler::handle_get_client(req, sender, session, shard)
+                .await
+        }
+        GET_CLIENTS_CODE => {
+            handlers::system::get_clients_handler::handle_get_clients(sender, session, shard).await
+        }
+        GET_SNAPSHOT_FILE_CODE => {
+            let req: GetSnapshotRequest = decode(frame.payload)?;
+            handlers::system::get_snapshot_handler::handle_get_snapshot(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        GET_CLUSTER_METADATA_CODE => {
+            handlers::cluster::get_cluster_metadata_handler::handle_get_cluster_metadata(
+                sender, session, shard,
+            )
+            .await
+        }
+
+        // Streams
+        GET_STREAM_CODE => {
+            let req: GetStreamRequest = decode(frame.payload)?;
+            handlers::streams::get_stream_handler::handle_get_stream(req, sender, session, shard)
+                .await
+        }
+        GET_STREAMS_CODE => {
+            handlers::streams::get_streams_handler::handle_get_streams(sender, session, shard)
+                .await
+        }
+        CREATE_STREAM_CODE => {
+            let req: CreateStreamRequest = decode(frame.payload)?;
+            handlers::streams::create_stream_handler::handle_create_stream(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_STREAM_CODE => {
+            let req: DeleteStreamRequest = decode(frame.payload)?;
+            handlers::streams::delete_stream_handler::handle_delete_stream(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        UPDATE_STREAM_CODE => {
+            let req: UpdateStreamRequest = decode(frame.payload)?;
+            handlers::streams::update_stream_handler::handle_update_stream(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        PURGE_STREAM_CODE => {
+            let req: PurgeStreamRequest = decode(frame.payload)?;
+            handlers::streams::purge_stream_handler::handle_purge_stream(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Topics
+        GET_TOPIC_CODE => {
+            let req: GetTopicRequest = decode(frame.payload)?;
+            handlers::topics::get_topic_handler::handle_get_topic(req, sender, session, shard)
+                .await
+        }
+        GET_TOPICS_CODE => {
+            let req: GetTopicsRequest = decode(frame.payload)?;
+            handlers::topics::get_topics_handler::handle_get_topics(req, sender, session, shard)
+                .await
+        }
+        CREATE_TOPIC_CODE => {
+            let req: CreateTopicRequest = decode(frame.payload)?;
+            handlers::topics::create_topic_handler::handle_create_topic(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_TOPIC_CODE => {
+            let req: DeleteTopicRequest = decode(frame.payload)?;
+            handlers::topics::delete_topic_handler::handle_delete_topic(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        UPDATE_TOPIC_CODE => {
+            let req: UpdateTopicRequest = decode(frame.payload)?;
+            handlers::topics::update_topic_handler::handle_update_topic(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        PURGE_TOPIC_CODE => {
+            let req: PurgeTopicRequest = decode(frame.payload)?;
+            handlers::topics::purge_topic_handler::handle_purge_topic(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Partitions
+        CREATE_PARTITIONS_CODE => {
+            let req: CreatePartitionsRequest = decode(frame.payload)?;
+            handlers::partitions::create_partitions_handler::handle_create_partitions(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_PARTITIONS_CODE => {
+            let req: DeletePartitionsRequest = decode(frame.payload)?;
+            handlers::partitions::delete_partitions_handler::handle_delete_partitions(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Segments
+        DELETE_SEGMENTS_CODE => {
+            let req: DeleteSegmentsRequest = decode(frame.payload)?;
+            handlers::segments::delete_segments_handler::handle_delete_segments(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Messages (PollMessages + FlushUnsavedBuffer; SendMessages handled above)
+        POLL_MESSAGES_CODE => {
+            let req: PollMessagesRequest = decode(frame.payload)?;
+            handlers::messages::poll_messages_handler::handle_poll_messages(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        FLUSH_UNSAVED_BUFFER_CODE => {
+            let req: FlushUnsavedBufferRequest = decode(frame.payload)?;
+            handlers::messages::flush_unsaved_buffer_handler::handle_flush_unsaved_buffer(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Consumer Offsets
+        GET_CONSUMER_OFFSET_CODE => {
+            let req: GetConsumerOffsetRequest = decode(frame.payload)?;
+            handlers::consumer_offsets::get_consumer_offset_handler::handle_get_consumer_offset(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        STORE_CONSUMER_OFFSET_CODE => {
+            let req: StoreConsumerOffsetRequest = decode(frame.payload)?;
+            handlers::consumer_offsets::store_consumer_offset_handler::handle_store_consumer_offset(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_CONSUMER_OFFSET_CODE => {
+            let req: DeleteConsumerOffsetRequest = decode(frame.payload)?;
+            handlers::consumer_offsets::delete_consumer_offset_handler::handle_delete_consumer_offset(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Consumer Groups
+        GET_CONSUMER_GROUP_CODE => {
+            let req: GetConsumerGroupRequest = decode(frame.payload)?;
+            handlers::consumer_groups::get_consumer_group_handler::handle_get_consumer_group(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        GET_CONSUMER_GROUPS_CODE => {
+            let req: GetConsumerGroupsRequest = decode(frame.payload)?;
+            handlers::consumer_groups::get_consumer_groups_handler::handle_get_consumer_groups(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        CREATE_CONSUMER_GROUP_CODE => {
+            let req: CreateConsumerGroupRequest = decode(frame.payload)?;
+            handlers::consumer_groups::create_consumer_group_handler::handle_create_consumer_group(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_CONSUMER_GROUP_CODE => {
+            let req: DeleteConsumerGroupRequest = decode(frame.payload)?;
+            handlers::consumer_groups::delete_consumer_group_handler::handle_delete_consumer_group(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        JOIN_CONSUMER_GROUP_CODE => {
+            let req: JoinConsumerGroupRequest = decode(frame.payload)?;
+            handlers::consumer_groups::join_consumer_group_handler::handle_join_consumer_group(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        LEAVE_CONSUMER_GROUP_CODE => {
+            let req: LeaveConsumerGroupRequest = decode(frame.payload)?;
+            handlers::consumer_groups::leave_consumer_group_handler::handle_leave_consumer_group(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        // Users
+        GET_USER_CODE => {
+            let req: GetUserRequest = decode(frame.payload)?;
+            handlers::users::get_user_handler::handle_get_user(req, sender, session, shard).await
+        }
+        GET_USERS_CODE => {
+            handlers::users::get_users_handler::handle_get_users(sender, session, shard).await
+        }
+        CREATE_USER_CODE => {
+            let req: CreateUserRequest = decode(frame.payload)?;
+            handlers::users::create_user_handler::handle_create_user(req, sender, session, shard)
+                .await
+        }
+        DELETE_USER_CODE => {
+            let req: DeleteUserRequest = decode(frame.payload)?;
+            handlers::users::delete_user_handler::handle_delete_user(req, sender, session, shard)
+                .await
+        }
+        UPDATE_USER_CODE => {
+            let req: UpdateUserRequest = decode(frame.payload)?;
+            handlers::users::update_user_handler::handle_update_user(req, sender, session, shard)
+                .await
+        }
+        UPDATE_PERMISSIONS_CODE => {
+            let req: UpdatePermissionsRequest = decode(frame.payload)?;
+            handlers::users::update_permissions_handler::handle_update_permissions(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        CHANGE_PASSWORD_CODE => {
+            let req: ChangePasswordRequest = decode(frame.payload)?;
+            handlers::users::change_password_handler::handle_change_password(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        LOGIN_USER_CODE => {
+            let req: LoginUserRequest = decode(frame.payload)?;
+            handlers::users::login_user_handler::handle_login_user(req, sender, session, shard)
+                .await
+        }
+        LOGOUT_USER_CODE => {
+            handlers::users::logout_user_handler::handle_logout_user(sender, session, shard).await
+        }
+
+        // Personal Access Tokens
+        GET_PERSONAL_ACCESS_TOKENS_CODE => {
+            handlers::personal_access_tokens::get_personal_access_tokens_handler::handle_get_personal_access_tokens(
+                sender, session, shard,
+            )
+            .await
+        }
+        CREATE_PERSONAL_ACCESS_TOKEN_CODE => {
+            let req: CreatePersonalAccessTokenRequest = decode(frame.payload)?;
+            handlers::personal_access_tokens::create_personal_access_token_handler::handle_create_personal_access_token(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        DELETE_PERSONAL_ACCESS_TOKEN_CODE => {
+            let req: DeletePersonalAccessTokenRequest = decode(frame.payload)?;
+            handlers::personal_access_tokens::delete_personal_access_token_handler::handle_delete_personal_access_token(
+                req, sender, session, shard,
+            )
+            .await
+        }
+        LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE => {
+            let req: LoginWithPersonalAccessTokenRequest = decode(frame.payload)?;
+            handlers::personal_access_tokens::login_with_personal_access_token_handler::handle_login_with_personal_access_token(
+                req, sender, session, shard,
+            )
+            .await
+        }
+
+        _ => {
+            error!("Unknown command code: {}", frame.code);
+            Err(IggyError::InvalidCommand)
+        }
+    }
+}
diff --git a/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs b/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
index aa29f8512..466945f39 100644
--- a/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
+++ b/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
@@ -16,49 +16,45 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
-use iggy_common::get_cluster_metadata::GetClusterMetadata;
-use iggy_common::{BytesSerializable, IggyError, SenderKind};
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::system::get_cluster_metadata::{
+    ClusterMetadataResponse, ClusterNodeResponse,
+};
+use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for GetClusterMetadata {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CLUSTER_METADATA_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_get_cluster_metadata", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+#[instrument(skip_all, name = "trace_get_cluster_metadata", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_get_cluster_metadata(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_cluster_metadata");
+    shard.ensure_authenticated(session)?;
 
-        let cluster_metadata = shard.get_cluster_metadata();
-
-        let response = cluster_metadata.to_bytes();
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let cluster_metadata = shard.get_cluster_metadata();
 
-impl BinaryServerCommand for GetClusterMetadata {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetClusterMetadata(get_cluster_metadata) => Ok(get_cluster_metadata),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let response = ClusterMetadataResponse {
+        name: cluster_metadata.name,
+        nodes: cluster_metadata
+            .nodes
+            .into_iter()
+            .map(|node| ClusterNodeResponse {
+                name: node.name,
+                ip: node.ip,
+                tcp_port: node.endpoints.tcp,
+                quic_port: node.endpoints.quic,
+                http_port: node.endpoints.http,
+                websocket_port: node.endpoints.websocket,
+                role: node.role as u8,
+                status: node.status as u8,
+            })
+            .collect(),
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
index 2945bb41a..3dc1e2591 100644
--- a/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
@@ -16,64 +16,60 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::consumer_groups::CreateConsumerGroupRequest;
+use iggy_binary_protocol::responses::consumer_groups::ConsumerGroupResponse;
 use iggy_common::create_consumer_group::CreateConsumerGroup;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreateConsumerGroup {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_CONSUMER_GROUP_CODE
-    }
+#[instrument(skip_all, name = "trace_create_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_consumer_group(
+    req: CreateConsumerGroupRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: create_consumer_group, stream_id: {stream_id}, topic_id: {topic_id}, name: {}",
+        req.name.as_str()
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_create_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = CreateConsumerGroup {
+        stream_id,
+        topic_id,
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-        let request =
-            ShardRequest::control_plane(ShardRequestPayload::CreateConsumerGroupRequest {
-                user_id: session.get_user_id(),
-                command: self,
-            });
+    let request = ShardRequest::control_plane(ShardRequestPayload::CreateConsumerGroupRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreateConsumerGroupResponse(data) => {
-                sender
-                    .send_ok_response(&mapper::map_consumer_group_from_response(&data))
-                    .await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreateConsumerGroupResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreateConsumerGroupResponse(data) => {
+            let response = ConsumerGroupResponse {
+                id: data.id,
+                partitions_count: data.partitions_count,
+                members_count: 0,
+                name: WireName::new(data.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+            };
+            sender.send_ok_response(&response.to_bytes()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreateConsumerGroupResponse"),
     }
-}
 
-impl BinaryServerCommand for CreateConsumerGroup {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreateConsumerGroup(create_consumer_group) => Ok(create_consumer_group),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
index ffb35d0ec..35a1b84a3 100644
--- a/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
@@ -16,61 +16,50 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::consumer_groups::DeleteConsumerGroupRequest;
 use iggy_common::delete_consumer_group::DeleteConsumerGroup;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeleteConsumerGroup {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_CONSUMER_GROUP_CODE
-    }
+#[instrument(skip_all, name = "trace_delete_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_consumer_group(
+    req: DeleteConsumerGroupRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let group_id = wire_id_to_identifier(&req.group_id)?;
+    debug!(
+        "session: {session}, command: delete_consumer_group, stream_id: {stream_id}, topic_id: {topic_id}, group_id: {group_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_delete_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = DeleteConsumerGroup {
+        stream_id,
+        topic_id,
+        group_id,
+    };
 
-        let request =
-            ShardRequest::control_plane(ShardRequestPayload::DeleteConsumerGroupRequest {
-                user_id: session.get_user_id(),
-                command: self,
-            });
+    let request = ShardRequest::control_plane(ShardRequestPayload::DeleteConsumerGroupRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeleteConsumerGroupResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeleteConsumerGroupResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeleteConsumerGroupResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeleteConsumerGroupResponse"),
     }
-}
 
-impl BinaryServerCommand for DeleteConsumerGroup {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteConsumerGroup(delete_consumer_group) => Ok(delete_consumer_group),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
index c0eddb8af..3a54bd5ab 100644
--- a/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
@@ -16,59 +16,64 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::consumer_groups::GetConsumerGroupRequest;
+use iggy_binary_protocol::responses::consumer_groups::{
+    ConsumerGroupDetailsResponse, ConsumerGroupMemberResponse, ConsumerGroupResponse,
+};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_consumer_group::GetConsumerGroup;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetConsumerGroup {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CONSUMER_GROUP_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+pub async fn handle_get_consumer_group(
+    req: GetConsumerGroupRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let group_id = wire_id_to_identifier(&req.group_id)?;
+    debug!(
+        "session: {session}, command: get_consumer_group, stream_id: {stream_id}, topic_id: {topic_id}, group_id: {group_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-        let Some(consumer_group) = shard.metadata.query_consumer_group(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-            &self.group_id,
-        )?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let response = mapper::map_consumer_group_from_meta(&consumer_group);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let Some(consumer_group) = shard.metadata.query_consumer_group(
+        session.get_user_id(),
+        &stream_id,
+        &topic_id,
+        &group_id,
+    )?
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-impl BinaryServerCommand for GetConsumerGroup {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetConsumerGroup(get_consumer_group) => Ok(get_consumer_group),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let members: Vec<ConsumerGroupMemberResponse> = consumer_group
+        .members
+        .iter()
+        .map(|(_, member)| ConsumerGroupMemberResponse {
+            id: member.id as u32,
+            partitions_count: member.partitions.len() as u32,
+            partitions: member.partitions.iter().map(|&p| p as u32).collect(),
+        })
+        .collect();
+    let response = ConsumerGroupDetailsResponse {
+        group: ConsumerGroupResponse {
+            id: consumer_group.id as u32,
+            partitions_count: consumer_group.partitions.len() as u32,
+            members_count: consumer_group.members.len() as u32,
+            name: WireName::new(consumer_group.name.as_ref())
+                .map_err(|_| IggyError::InvalidCommand)?,
+        },
+        members,
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs b/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
index f798605c5..2d64c8d00 100644
--- a/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
@@ -16,58 +16,54 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use bytes::Bytes;
-use iggy_common::get_consumer_groups::GetConsumerGroups;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::consumer_groups::GetConsumerGroupsRequest;
+use iggy_binary_protocol::responses::consumer_groups::{
+    ConsumerGroupResponse, GetConsumerGroupsResponse,
+};
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetConsumerGroups {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CONSUMER_GROUPS_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+pub async fn handle_get_consumer_groups(
+    req: GetConsumerGroupsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: get_consumer_groups, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-        let Some(consumer_groups) = shard.metadata.query_consumer_groups(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-        )?
-        else {
-            sender.send_ok_response(&Bytes::new()).await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let response = mapper::map_consumer_groups(&consumer_groups);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let Some(consumer_groups) =
+        shard
+            .metadata
+            .query_consumer_groups(session.get_user_id(), &stream_id, &topic_id)?
+    else {
+        sender.send_ok_response(&Bytes::new()).await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-impl BinaryServerCommand for GetConsumerGroups {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetConsumerGroups(get_consumer_groups) => Ok(get_consumer_groups),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let groups: Vec<ConsumerGroupResponse> = consumer_groups
+        .iter()
+        .map(|cg| {
+            Ok(ConsumerGroupResponse {
+                id: cg.id as u32,
+                partitions_count: cg.partitions.len() as u32,
+                members_count: cg.members.len() as u32,
+                name: WireName::new(cg.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+            })
+        })
+        .collect::<Result<_, IggyError>>()?;
+    let response = GetConsumerGroupsResponse { groups };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
index 18b220ad1..f3adcb6e6 100644
--- a/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
@@ -16,61 +16,51 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::consumer_groups::JoinConsumerGroupRequest;
 use iggy_common::join_consumer_group::JoinConsumerGroup;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for JoinConsumerGroup {
-    fn code(&self) -> u32 {
-        iggy_common::JOIN_CONSUMER_GROUP_CODE
-    }
+#[instrument(skip_all, name = "trace_join_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_join_consumer_group(
+    req: JoinConsumerGroupRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let group_id = wire_id_to_identifier(&req.group_id)?;
+    debug!(
+        "session: {session}, command: join_consumer_group, stream_id: {stream_id}, topic_id: {topic_id}, group_id: {group_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_join_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string(), iggy_group_id = self.group_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = JoinConsumerGroup {
+        stream_id,
+        topic_id,
+        group_id,
+    };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::JoinConsumerGroupRequest {
-            user_id: session.get_user_id(),
-            client_id: session.client_id,
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::JoinConsumerGroupRequest {
+        user_id: session.get_user_id(),
+        client_id: session.client_id,
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::JoinConsumerGroupResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected JoinConsumerGroupResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::JoinConsumerGroupResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected JoinConsumerGroupResponse"),
     }
-}
 
-impl BinaryServerCommand for JoinConsumerGroup {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::JoinConsumerGroup(join_consumer_group) => Ok(join_consumer_group),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
index 3134086d6..a522e6fb0 100644
--- a/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
@@ -16,61 +16,51 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::consumer_groups::LeaveConsumerGroupRequest;
 use iggy_common::leave_consumer_group::LeaveConsumerGroup;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for LeaveConsumerGroup {
-    fn code(&self) -> u32 {
-        iggy_common::LEAVE_CONSUMER_GROUP_CODE
-    }
+#[instrument(skip_all, name = "trace_leave_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_leave_consumer_group(
+    req: LeaveConsumerGroupRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let group_id = wire_id_to_identifier(&req.group_id)?;
+    debug!(
+        "session: {session}, command: leave_consumer_group, stream_id: {stream_id}, topic_id: {topic_id}, group_id: {group_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_leave_consumer_group", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string(), iggy_group_id = self.group_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = LeaveConsumerGroup {
+        stream_id,
+        topic_id,
+        group_id,
+    };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::LeaveConsumerGroupRequest {
-            user_id: session.get_user_id(),
-            client_id: session.client_id,
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::LeaveConsumerGroupRequest {
+        user_id: session.get_user_id(),
+        client_id: session.client_id,
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::LeaveConsumerGroupResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected LeaveConsumerGroupResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::LeaveConsumerGroupResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected LeaveConsumerGroupResponse"),
     }
-}
 
-impl BinaryServerCommand for LeaveConsumerGroup {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::LeaveConsumerGroup(leave_consumer_group) => Ok(leave_consumer_group),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
index 0d986e921..459198a06 100644
--- a/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
@@ -16,59 +16,41 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::{HandlerResult, wire_consumer_to_consumer, wire_id_to_identifier};
 use crate::binary::handlers::consumer_offsets::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
-use iggy_common::delete_consumer_offset::DeleteConsumerOffset;
+use iggy_binary_protocol::requests::consumer_offsets::DeleteConsumerOffsetRequest;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for DeleteConsumerOffset {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_CONSUMER_OFFSET_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        let topic = shard.resolve_topic_for_delete_consumer_offset(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-        )?;
-        shard
-            .delete_consumer_offset(session.client_id, self.consumer, topic, self.partition_id)
-            .await
-            .error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to delete consumer offset for topic with ID: {} in stream with ID: {} partition ID: {:#?}, session: {}",
-                self.topic_id, self.stream_id, self.partition_id, session
-            ))?;
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for DeleteConsumerOffset {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteConsumerOffset(delete_consumer_offset) => {
-                Ok(delete_consumer_offset)
-            }
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+pub async fn handle_delete_consumer_offset(
+    req: DeleteConsumerOffsetRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let consumer = wire_consumer_to_consumer(&req.consumer)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: delete_consumer_offset, stream_id: {stream_id}, topic_id: {topic_id}, partition_id: {:?}",
+        req.partition_id
+    );
+    shard.ensure_authenticated(session)?;
+    let topic = shard.resolve_topic_for_delete_consumer_offset(
+        session.get_user_id(),
+        &stream_id,
+        &topic_id,
+    )?;
+    shard
+        .delete_consumer_offset(session.client_id, consumer, topic, req.partition_id)
+        .await
+        .error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to delete consumer offset for topic with ID: {} in stream with ID: {} partition ID: {:#?}, session: {}",
+            topic_id, stream_id, req.partition_id, session
+        ))?;
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
index 1e207d78a..97eefd75b 100644
--- a/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
@@ -16,77 +16,65 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_consumer_to_consumer, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::message::ResolvedTopic;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::consumer_offsets::GetConsumerOffsetRequest;
+use iggy_binary_protocol::responses::consumer_offsets::ConsumerOffsetResponse;
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_consumer_offset::GetConsumerOffset;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetConsumerOffset {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CONSUMER_OFFSET_CODE
-    }
+pub async fn handle_get_consumer_offset(
+    req: GetConsumerOffsetRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let consumer = wire_consumer_to_consumer(&req.consumer)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: get_consumer_offset, stream_id: {stream_id}, topic_id: {topic_id}, partition_id: {:?}",
+        req.partition_id
+    );
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let Some(resolved) =
+        shard
+            .metadata
+            .resolve_for_consumer_offset(session.get_user_id(), &stream_id, &topic_id)?
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-        let Some(resolved) = shard.metadata.resolve_for_consumer_offset(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-        )?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
+    let topic = ResolvedTopic {
+        stream_id: resolved.stream_id,
+        topic_id: resolved.topic_id,
+    };
 
-        let topic = ResolvedTopic {
-            stream_id: resolved.stream_id,
-            topic_id: resolved.topic_id,
-        };
+    let Ok(offset) = shard
+        .get_consumer_offset(session.client_id, consumer, topic, req.partition_id)
+        .await
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-        let Ok(offset) = shard
-            .get_consumer_offset(session.client_id, self.consumer, topic, self.partition_id)
-            .await
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
+    let Some(offset) = offset else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-        let Some(offset) = offset else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let offset = mapper::map_consumer_offset(&offset);
-        sender.send_ok_response(&offset).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for GetConsumerOffset {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetConsumerOffset(get_consumer_offset) => Ok(get_consumer_offset),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let response = ConsumerOffsetResponse {
+        partition_id: offset.partition_id,
+        current_offset: offset.current_offset,
+        stored_offset: offset.stored_offset,
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
index a0d2f2cbd..87eb4c5e5 100644
--- a/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
@@ -18,63 +18,47 @@
 
 use std::rc::Rc;
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::{HandlerResult, wire_consumer_to_consumer, wire_id_to_identifier};
 use crate::binary::handlers::consumer_offsets::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
+use iggy_binary_protocol::requests::consumer_offsets::StoreConsumerOffsetRequest;
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::store_consumer_offset::StoreConsumerOffset;
 use tracing::debug;
 
-impl ServerCommandHandler for StoreConsumerOffset {
-    fn code(&self) -> u32 {
-        iggy_common::STORE_CONSUMER_OFFSET_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        let topic = shard.resolve_topic_for_store_consumer_offset(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-        )?;
-        shard
-            .store_consumer_offset(
-                session.client_id,
-                self.consumer,
-                topic,
-                self.partition_id,
-                self.offset,
-            )
-            .await
-            .error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to store consumer offset for stream_id: {}, topic_id: {}, partition_id: {:?}, offset: {}, session: {}",
-                self.stream_id, self.topic_id, self.partition_id, self.offset, session
-            ))?;
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for StoreConsumerOffset {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::StoreConsumerOffset(store_consumer_offset) => Ok(store_consumer_offset),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+pub async fn handle_store_consumer_offset(
+    req: StoreConsumerOffsetRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let consumer = wire_consumer_to_consumer(&req.consumer)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: store_consumer_offset, stream_id: {stream_id}, topic_id: {topic_id}, partition_id: {:?}, offset: {}",
+        req.partition_id, req.offset
+    );
+    shard.ensure_authenticated(session)?;
+    let topic = shard.resolve_topic_for_store_consumer_offset(
+        session.get_user_id(),
+        &stream_id,
+        &topic_id,
+    )?;
+    shard
+        .store_consumer_offset(
+            session.client_id,
+            consumer,
+            topic,
+            req.partition_id,
+            req.offset,
+        )
+        .await
+        .error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to store consumer offset for stream_id: {}, topic_id: {}, partition_id: {:?}, offset: {}, session: {}",
+            stream_id, topic_id, req.partition_id, req.offset, session
+        ))?;
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs b/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
index 300ecc672..79f8dbd4e 100644
--- a/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
+++ b/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
@@ -16,71 +16,50 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::binary::handlers::messages::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
 use crate::shard::IggyShard;
 use crate::shard::transmission::message::ResolvedPartition;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
-use iggy_common::{FlushUnsavedBuffer, IggyError, SenderKind};
+use iggy_binary_protocol::requests::messages::FlushUnsavedBufferRequest;
+use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for FlushUnsavedBuffer {
-    fn code(&self) -> u32 {
-        iggy_common::FLUSH_UNSAVED_BUFFER_CODE
-    }
+#[instrument(skip_all, name = "trace_flush_unsaved_buffer", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_partition_id = req.partition_id, iggy_fsync = req.fsync))]
+pub async fn handle_flush_unsaved_buffer(
+    req: FlushUnsavedBufferRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let partition_id = req.partition_id;
+    let fsync = req.fsync;
+    debug!(
+        "session: {session}, command: flush_unsaved_buffer, stream_id: {stream_id}, topic_id: {topic_id}, partition_id: {partition_id}, fsync: {fsync}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_flush_unsaved_buffer", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string(), iggy_partition_id = self.partition_id, iggy_fsync = self.fsync))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let user_id = session.get_user_id();
+    let topic = shard.resolve_topic(&stream_id, &topic_id)?;
+    let partition = ResolvedPartition {
+        stream_id: topic.stream_id,
+        topic_id: topic.topic_id,
+        partition_id: partition_id as usize,
+    };
 
-        let user_id = session.get_user_id();
-        let stream_id_log = self.stream_id.clone();
-        let topic_id_log = self.topic_id.clone();
-        let partition_id = self.partition_id;
-        let fsync = self.fsync;
-
-        let topic = shard.resolve_topic(&self.stream_id, &self.topic_id)?;
-        let partition = ResolvedPartition {
-            stream_id: topic.stream_id,
-            topic_id: topic.topic_id,
-            partition_id: partition_id as usize,
-        };
-
-        shard
-            .flush_unsaved_buffer(user_id, partition, fsync)
-            .await
-            .error(|e: &IggyError| {
-                format!(
-                    "{COMPONENT} (error: {e}) - failed to flush unsaved buffer for stream_id: {}, topic_id: {}, partition_id: {}, session: {}",
-                    stream_id_log, topic_id_log, partition_id, session
-                )
-            })?;
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for FlushUnsavedBuffer {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::FlushUnsavedBuffer(flush_unsaved_buffer) => Ok(flush_unsaved_buffer),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    shard
+        .flush_unsaved_buffer(user_id, partition, fsync)
+        .await
+        .error(|e: &IggyError| {
+            format!(
+                "{COMPONENT} (error: {e}) - failed to flush unsaved buffer for stream_id: {}, topic_id: {}, partition_id: {}, session: {}",
+                stream_id, topic_id, partition_id, session
+            )
+        })?;
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/messages/poll_messages_handler.rs b/core/server/src/binary/handlers/messages/poll_messages_handler.rs
index 71388d75b..ca1b11c14 100644
--- a/core/server/src/binary/handlers/messages/poll_messages_handler.rs
+++ b/core/server/src/binary/handlers/messages/poll_messages_handler.rs
@@ -16,96 +16,72 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
+use crate::binary::dispatch::{
+    HandlerResult, wire_consumer_to_consumer, wire_id_to_identifier, wire_polling_to_strategy,
 };
-use crate::binary::handlers::utils::receive_and_validate;
 use crate::shard::IggyShard;
 use crate::shard::system::messages::PollingArgs;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::messages::PollMessagesRequest;
 use iggy_common::SenderKind;
-use iggy_common::{IggyError, PollMessages, PooledBuffer};
+use iggy_common::{IggyError, PooledBuffer};
 use std::rc::Rc;
 use tracing::{debug, trace};
 
-impl ServerCommandHandler for PollMessages {
-    fn code(&self) -> u32 {
-        iggy_common::POLL_MESSAGES_CODE
-    }
+pub async fn handle_poll_messages(
+    req: PollMessagesRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let consumer = wire_consumer_to_consumer(&req.consumer)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    let strategy = wire_polling_to_strategy(&req.strategy)?;
+    let partition_id = req.partition_id;
+    let count = req.count;
+    let auto_commit = req.auto_commit;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        let PollMessages {
-            consumer,
-            partition_id,
-            strategy,
-            count,
-            auto_commit,
-            stream_id,
-            topic_id,
-        } = self;
-        let args = PollingArgs::new(strategy, count, auto_commit);
+    debug!(
+        "session: {session}, command: poll_messages, stream_id: {stream_id}, topic_id: {topic_id}, partition_id: {partition_id:?}"
+    );
+    shard.ensure_authenticated(session)?;
 
-        let user_id = session.get_user_id();
-        let client_id = session.client_id;
-        let topic = shard.resolve_topic_for_poll(user_id, &stream_id, &topic_id)?;
-        let (metadata, mut batch) = shard
-            .poll_messages(client_id, topic, consumer, partition_id, args)
-            .await?;
+    let args = PollingArgs::new(strategy, count, auto_commit);
 
-        // Collect all chunks first into a Vec to extend their lifetimes.
-        // This ensures the Bytes (in reality Arc<[u8]>) references from each IggyMessagesBatch stay alive
-        // throughout the async vectored I/O operation, preventing "borrowed value does not live
-        // long enough" errors while optimizing transmission by using larger chunks.
+    let user_id = session.get_user_id();
+    let client_id = session.client_id;
+    let topic = shard.resolve_topic_for_poll(user_id, &stream_id, &topic_id)?;
+    let (metadata, mut batch) = shard
+        .poll_messages(client_id, topic, consumer, partition_id, args)
+        .await?;
 
-        // 4 bytes for partition_id + 8 bytes for current_offset + 4 bytes for messages_count + size of all batches.
-        let response_length = 4 + 8 + 4 + batch.size();
-        let response_length_bytes = response_length.to_le_bytes();
+    let response_length = 4 + 8 + 4 + batch.size();
+    let response_length_bytes = response_length.to_le_bytes();
 
-        let mut bufs = Vec::with_capacity(batch.containers_count() + 3);
-        let mut partition_id_buf = PooledBuffer::with_capacity(4);
-        let mut current_offset_buf = PooledBuffer::with_capacity(8);
-        let mut count_buf = PooledBuffer::with_capacity(4);
-        partition_id_buf.put_u32_le(metadata.partition_id);
-        current_offset_buf.put_u64_le(metadata.current_offset);
-        count_buf.put_u32_le(batch.count());
+    let mut bufs = Vec::with_capacity(batch.containers_count() + 3);
+    let mut partition_id_buf = PooledBuffer::with_capacity(4);
+    let mut current_offset_buf = PooledBuffer::with_capacity(8);
+    let mut count_buf = PooledBuffer::with_capacity(4);
+    partition_id_buf.put_u32_le(metadata.partition_id);
+    current_offset_buf.put_u64_le(metadata.current_offset);
+    count_buf.put_u32_le(batch.count());
 
-        bufs.push(partition_id_buf);
-        bufs.push(current_offset_buf);
-        bufs.push(count_buf);
+    bufs.push(partition_id_buf);
+    bufs.push(current_offset_buf);
+    bufs.push(count_buf);
 
-        batch.iter_mut().for_each(|m| {
-            bufs.push(m.take_messages());
-        });
-        trace!(
-            "Sending {} messages to client ({} bytes) to client",
-            batch.count(),
-            response_length
-        );
+    batch.iter_mut().for_each(|m| {
+        bufs.push(m.take_messages());
+    });
+    trace!(
+        "Sending {} messages to client ({} bytes) to client",
+        batch.count(),
+        response_length
+    );
 
-        sender
-            .send_ok_response_vectored(&response_length_bytes, bufs)
-            .await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for PollMessages {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::PollMessages(poll_messages) => Ok(poll_messages),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    sender
+        .send_ok_response_vectored(&response_length_bytes, bufs)
+        .await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/messages/send_messages_handler.rs b/core/server/src/binary/handlers/messages/send_messages_handler.rs
index 91a5ea1e7..2964dcfce 100644
--- a/core/server/src/binary/handlers/messages/send_messages_handler.rs
+++ b/core/server/src/binary/handlers/messages/send_messages_handler.rs
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-use crate::binary::command::{BinaryServerCommand, HandlerResult, ServerCommandHandler};
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::shard::transmission::message::{ResolvedPartition, ShardRequest, ShardRequestPayload};
 use crate::streaming::segments::{IggyIndexesMut, IggyMessagesBatchMut};
@@ -29,205 +29,176 @@ use iggy_common::SenderKind;
 use iggy_common::Sizeable;
 use iggy_common::sharding::IggyNamespace;
 use iggy_common::{INDEX_SIZE, PartitioningKind};
-use iggy_common::{IggyError, Partitioning, SendMessages, Validatable};
+use iggy_common::{IggyError, Partitioning, Validatable};
 use std::rc::Rc;
 use tracing::{debug, error, info, instrument};
 
-impl ServerCommandHandler for SendMessages {
-    fn code(&self) -> u32 {
-        iggy_common::SEND_MESSAGES_CODE
+#[instrument(skip_all, name = "trace_send_messages", fields(
+    iggy_user_id = session.get_user_id(),
+    iggy_client_id = session.client_id,
+))]
+pub async fn handle_send_messages(
+    sender: &mut SenderKind,
+    length: u32,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    shard.ensure_authenticated(session)?;
+    // `length` is the payload size (frame length minus the 4-byte code,
+    // already subtracted by the transport layer before calling dispatch).
+    let total_payload_size = length as usize;
+    let metadata_len_field_size = std::mem::size_of::<u32>();
+
+    let metadata_length_buffer = PooledBuffer::with_capacity(4);
+    let (result, metadata_len_buf) = sender.read(metadata_length_buffer.slice(0..4)).await;
+    let metadata_len_buf = metadata_len_buf.into_inner();
+    result?;
+    let metadata_size = u32::from_le_bytes(
+        metadata_len_buf[..]
+            .try_into()
+            .map_err(|_| IggyError::InvalidNumberEncoding)?,
+    );
+    if metadata_size as usize > total_payload_size {
+        return Err(IggyError::InvalidCommand);
     }
 
-    #[instrument(skip_all, name = "trace_send_messages", fields(
-        iggy_user_id = session.get_user_id(),
-        iggy_client_id = session.client_id,
-        iggy_stream_id = self.stream_id.as_string(),
-        iggy_topic_id = self.topic_id.as_string(),
-        partitioning = %self.partitioning
-    ))]
-    async fn handle(
-        mut self,
-        sender: &mut SenderKind,
-        length: u32,
-
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        shard.ensure_authenticated(session)?;
-        let total_payload_size = (length as usize)
-            .checked_sub(std::mem::size_of::<u32>())
-            .ok_or(IggyError::InvalidCommand)?;
-        let metadata_len_field_size = std::mem::size_of::<u32>();
-
-        let metadata_length_buffer = PooledBuffer::with_capacity(4);
-        let (result, metadata_len_buf) = sender.read(metadata_length_buffer.slice(0..4)).await;
-        let metadata_len_buf = metadata_len_buf.into_inner();
-        result?;
-        let metadata_size = u32::from_le_bytes(
-            metadata_len_buf[..]
-                .try_into()
-                .map_err(|_| IggyError::InvalidNumberEncoding)?,
-        );
+    let metadata_buffer = PooledBuffer::with_capacity(metadata_size as usize);
+    let (result, metadata_buf) = sender
+        .read(metadata_buffer.slice(0..metadata_size as usize))
+        .await;
+    result?;
+    let metadata_buf = metadata_buf.into_inner();
+
+    let mut element_size = 0;
+
+    let stream_id = Identifier::from_raw_bytes(&metadata_buf)?;
+    element_size += stream_id.get_size_bytes().as_bytes_usize();
+
+    let topic_id = Identifier::from_raw_bytes(
+        metadata_buf
+            .get(element_size..)
+            .ok_or(IggyError::InvalidCommand)?,
+    )?;
+    element_size += topic_id.get_size_bytes().as_bytes_usize();
+
+    let partitioning = Partitioning::from_raw_bytes(
+        metadata_buf
+            .get(element_size..)
+            .ok_or(IggyError::InvalidCommand)?,
+    )?;
+    element_size += partitioning.get_size_bytes().as_bytes_usize();
+
+    let messages_count = u32::from_le_bytes(
+        metadata_buf
+            .get(element_size..element_size + 4)
+            .ok_or(IggyError::InvalidCommand)?
+            .try_into()
+            .map_err(|_| IggyError::InvalidNumberEncoding)?,
+    );
+    let indexes_size = (messages_count as usize)
+        .checked_mul(INDEX_SIZE)
+        .ok_or(IggyError::InvalidCommand)?;
+    if indexes_size > total_payload_size {
+        return Err(IggyError::InvalidCommand);
+    }
 
-        let metadata_buffer = PooledBuffer::with_capacity(metadata_size as usize);
-        let (result, metadata_buf) = sender
-            .read(metadata_buffer.slice(0..metadata_size as usize))
-            .await;
-        result?;
-        let metadata_buf = metadata_buf.into_inner();
-
-        let mut element_size = 0;
-
-        let stream_id = Identifier::from_raw_bytes(&metadata_buf)?;
-        element_size += stream_id.get_size_bytes().as_bytes_usize();
-        self.stream_id = stream_id;
-
-        let topic_id = Identifier::from_raw_bytes(
-            metadata_buf
-                .get(element_size..)
-                .ok_or(IggyError::InvalidCommand)?,
-        )?;
-        element_size += topic_id.get_size_bytes().as_bytes_usize();
-        self.topic_id = topic_id;
-
-        let partitioning = Partitioning::from_raw_bytes(
-            metadata_buf
-                .get(element_size..)
-                .ok_or(IggyError::InvalidCommand)?,
-        )?;
-        element_size += partitioning.get_size_bytes().as_bytes_usize();
-        self.partitioning = partitioning;
-
-        let messages_count = u32::from_le_bytes(
-            metadata_buf
-                .get(element_size..element_size + 4)
+    let indexes_buffer = PooledBuffer::with_capacity(indexes_size);
+    let (result, indexes_buffer) = sender.read(indexes_buffer.slice(0..indexes_size)).await;
+    result?;
+    let indexes_buffer = indexes_buffer.into_inner();
+
+    let messages_size = total_payload_size
+        .checked_sub(metadata_size as usize)
+        .and_then(|s| s.checked_sub(indexes_size))
+        .and_then(|s| s.checked_sub(metadata_len_field_size))
+        .ok_or(IggyError::InvalidCommand)?;
+    let messages_buffer = PooledBuffer::with_capacity(messages_size);
+    let (result, messages_buffer) = sender.read(messages_buffer.slice(0..messages_size)).await;
+    result?;
+    let messages_buffer = messages_buffer.into_inner();
+
+    let indexes = IggyIndexesMut::from_bytes(indexes_buffer, 0);
+    let batch = IggyMessagesBatchMut::from_indexes_and_messages(indexes, messages_buffer);
+    batch.validate()?;
+
+    let topic = shard.resolve_topic_for_append(session.get_user_id(), &stream_id, &topic_id)?;
+
+    let partition_id = match partitioning.kind {
+        PartitioningKind::Balanced => shard
+            .metadata
+            .get_next_partition_id(topic.stream_id, topic.topic_id)
+            .ok_or(IggyError::TopicIdNotFound(
+                stream_id.clone(),
+                topic_id.clone(),
+            ))?,
+        PartitioningKind::PartitionId => u32::from_le_bytes(
+            partitioning
+                .value
+                .get(..4)
                 .ok_or(IggyError::InvalidCommand)?
                 .try_into()
                 .map_err(|_| IggyError::InvalidNumberEncoding)?,
-        );
-        let indexes_size = (messages_count as usize)
-            .checked_mul(INDEX_SIZE)
-            .ok_or(IggyError::InvalidCommand)?;
-        if indexes_size > total_payload_size {
-            return Err(IggyError::InvalidCommand);
+        ) as usize,
+        PartitioningKind::MessagesKey => {
+            let partitions_count = shard
+                .metadata
+                .partitions_count(topic.stream_id, topic.topic_id);
+            topics::helpers::calculate_partition_id_by_messages_key_hash(
+                partitions_count,
+                &partitioning.value,
+            )
         }
+    };
+
+    let namespace = IggyNamespace::new(topic.stream_id, topic.topic_id, partition_id);
+    let user_id = session.get_user_id();
+    let unsupported_socket_transfer = matches!(
+        partitioning.kind,
+        PartitioningKind::Balanced | PartitioningKind::MessagesKey
+    );
+    let enabled_socket_migration = shard.config.tcp.socket_migration;
+
+    if enabled_socket_migration
+        && !(session.is_migrated() || unsupported_socket_transfer)
+        && let Some(target_shard) = shard.find_shard(&namespace)
+        && target_shard.id != shard.id
+    {
+        debug!(
+            "TCP wrong shared detected: migrating from_shard {}, to_shard {}",
+            shard.id, target_shard.id
+        );
 
-        let indexes_buffer = PooledBuffer::with_capacity(indexes_size);
-        let (result, indexes_buffer) = sender.read(indexes_buffer.slice(0..indexes_size)).await;
-        result?;
-        let indexes_buffer = indexes_buffer.into_inner();
-
-        let messages_size = total_payload_size
-            .checked_sub(metadata_size as usize)
-            .and_then(|s| s.checked_sub(indexes_size))
-            .and_then(|s| s.checked_sub(metadata_len_field_size))
-            .ok_or(IggyError::InvalidCommand)?;
-        let messages_buffer = PooledBuffer::with_capacity(messages_size);
-
-        let (result, messages_buffer) = sender.read(messages_buffer.slice(0..messages_size)).await;
-        result?;
-        let messages_buffer = messages_buffer.into_inner();
-
-        let indexes = IggyIndexesMut::from_bytes(indexes_buffer, 0);
-        let batch = IggyMessagesBatchMut::from_indexes_and_messages(indexes, messages_buffer);
-        batch.validate()?;
-
-        let topic = shard.resolve_topic_for_append(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-        )?;
-
-        let partition_id = match self.partitioning.kind {
-            PartitioningKind::Balanced => shard
-                .metadata
-                .get_next_partition_id(topic.stream_id, topic.topic_id)
-                .ok_or(IggyError::TopicIdNotFound(
-                    self.stream_id.clone(),
-                    self.topic_id.clone(),
-                ))?,
-            PartitioningKind::PartitionId => u32::from_le_bytes(
-                self.partitioning
-                    .value
-                    .get(..4)
-                    .ok_or(IggyError::InvalidCommand)?
-                    .try_into()
-                    .map_err(|_| IggyError::InvalidNumberEncoding)?,
-            ) as usize,
-            PartitioningKind::MessagesKey => {
-                let partitions_count = shard
-                    .metadata
-                    .partitions_count(topic.stream_id, topic.topic_id);
-                topics::helpers::calculate_partition_id_by_messages_key_hash(
-                    partitions_count,
-                    &self.partitioning.value,
-                )
+        if let Some(fd) = sender.take_and_migrate_tcp() {
+            let payload = ShardRequestPayload::SocketTransfer {
+                fd,
+                from_shard: shard.id,
+                client_id: session.client_id,
+                user_id,
+                address: session.ip_address,
+                initial_data: batch,
+            };
+
+            let request = ShardRequest::data_plane(namespace, payload);
+
+            if let Err(e) = shard.send_to_data_plane(request).await {
+                error!("transfer socket to another shard failed, drop connection. {e:?}");
+                return Ok(HandlerResult::Finished);
             }
-        };
 
-        let namespace = IggyNamespace::new(topic.stream_id, topic.topic_id, partition_id);
-        let user_id = session.get_user_id();
-        let unsupported_socket_transfer = matches!(
-            self.partitioning.kind,
-            PartitioningKind::Balanced | PartitioningKind::MessagesKey
-        );
-        let enabled_socket_migration = shard.config.tcp.socket_migration;
-
-        if enabled_socket_migration
-            && !(session.is_migrated() || unsupported_socket_transfer)
-            && let Some(target_shard) = shard.find_shard(&namespace)
-            && target_shard.id != shard.id
-        {
-            debug!(
-                "TCP wrong shared detected: migrating from_shard {}, to_shard {}",
-                shard.id, target_shard.id
-            );
-
-            if let Some(fd) = sender.take_and_migrate_tcp() {
-                let payload = ShardRequestPayload::SocketTransfer {
-                    fd,
-                    from_shard: shard.id,
-                    client_id: session.client_id,
-                    user_id,
-                    address: session.ip_address,
-                    initial_data: batch,
-                };
-
-                let request = ShardRequest::data_plane(namespace, payload);
-
-                if let Err(e) = shard.send_to_data_plane(request).await {
-                    error!("transfer socket to another shard failed, drop connection. {e:?}");
-                    return Ok(HandlerResult::Finished);
-                }
-
-                info!("Sending socket transfer to shard {}", target_shard.id);
-                return Ok(HandlerResult::Migrated {
-                    to_shard: target_shard.id,
-                });
-            }
+            info!("Sending socket transfer to shard {}", target_shard.id);
+            return Ok(HandlerResult::Migrated {
+                to_shard: target_shard.id,
+            });
         }
-
-        let partition = ResolvedPartition {
-            stream_id: topic.stream_id,
-            topic_id: topic.topic_id,
-            partition_id,
-        };
-        shard.append_messages(partition, batch).await?;
-
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
     }
-}
 
-impl BinaryServerCommand for SendMessages {
-    async fn from_sender(
-        _sender: &mut SenderKind,
-        _code: u32,
-        _length: u32,
-    ) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        Ok(Self::default())
-    }
+    let partition = ResolvedPartition {
+        stream_id: topic.stream_id,
+        topic_id: topic.topic_id,
+        partition_id,
+    };
+    shard.append_messages(partition, batch).await?;
+
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/mod.rs b/core/server/src/binary/handlers/mod.rs
index 22807ba85..2c418bf59 100644
--- a/core/server/src/binary/handlers/mod.rs
+++ b/core/server/src/binary/handlers/mod.rs
@@ -27,4 +27,3 @@ pub mod streams;
 pub mod system;
 pub mod topics;
 pub mod users;
-mod utils;
diff --git a/core/server/src/binary/handlers/partitions/create_partitions_handler.rs b/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
index a5113b50a..94f7ed976 100644
--- a/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
+++ b/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
@@ -16,60 +16,50 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::partitions::CreatePartitionsRequest;
 use iggy_common::create_partitions::CreatePartitions;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreatePartitions {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_PARTITIONS_CODE
-    }
+#[instrument(skip_all, name = "trace_create_partitions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_partitions(
+    req: CreatePartitionsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: create_partitions, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_create_partitions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = CreatePartitions {
+        stream_id,
+        topic_id,
+        partitions_count: req.partitions_count,
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::CreatePartitionsRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::CreatePartitionsRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreatePartitionsResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreatePartitionsResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreatePartitionsResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreatePartitionsResponse"),
     }
-}
 
-impl BinaryServerCommand for CreatePartitions {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreatePartitions(create_partitions) => Ok(create_partitions),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs b/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
index 394f8379f..0b9fdc562 100644
--- a/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
+++ b/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
@@ -16,60 +16,50 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::partitions::DeletePartitionsRequest;
 use iggy_common::delete_partitions::DeletePartitions;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeletePartitions {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_PARTITIONS_CODE
-    }
+#[instrument(skip_all, name = "trace_delete_partitions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_partitions(
+    req: DeletePartitionsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: delete_partitions, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_delete_partitions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = DeletePartitions {
+        stream_id,
+        topic_id,
+        partitions_count: req.partitions_count,
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::DeletePartitionsRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::DeletePartitionsRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeletePartitionsResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeletePartitionsResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeletePartitionsResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeletePartitionsResponse"),
     }
-}
 
-impl BinaryServerCommand for DeletePartitions {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeletePartitions(delete_partitions) => Ok(delete_partitions),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
index 8036f02df..eca026b4b 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
@@ -16,66 +16,55 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::personal_access_tokens::CreatePersonalAccessTokenRequest;
+use iggy_binary_protocol::responses::personal_access_tokens::RawPersonalAccessTokenResponse;
 use iggy_common::create_personal_access_token::CreatePersonalAccessToken;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, IggyExpiry, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreatePersonalAccessToken {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_PERSONAL_ACCESS_TOKEN_CODE
-    }
+#[instrument(skip_all, name = "trace_create_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_personal_access_token(
+    req: CreatePersonalAccessTokenRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: create_personal_access_token, name: {}",
+        req.name.as_str()
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_create_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = CreatePersonalAccessToken {
+        name: req.name.to_string(),
+        expiry: IggyExpiry::from(req.expiry),
+    };
+    command.validate()?;
 
-        let request =
-            ShardRequest::control_plane(ShardRequestPayload::CreatePersonalAccessTokenRequest {
-                user_id: session.get_user_id(),
-                command: self,
-            });
+    let request =
+        ShardRequest::control_plane(ShardRequestPayload::CreatePersonalAccessTokenRequest {
+            user_id: session.get_user_id(),
+            command,
+        });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreatePersonalAccessTokenResponse(_, token) => {
-                sender
-                    .send_ok_response(&mapper::map_raw_pat(&token))
-                    .await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreatePersonalAccessTokenResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreatePersonalAccessTokenResponse(_, token) => {
+            let response = RawPersonalAccessTokenResponse {
+                token: WireName::new(token).map_err(|_| IggyError::InvalidCommand)?,
+            };
+            sender.send_ok_response(&response.to_bytes()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreatePersonalAccessTokenResponse"),
     }
-}
 
-impl BinaryServerCommand for CreatePersonalAccessToken {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreatePersonalAccessToken(create_personal_access_token) => {
-                Ok(create_personal_access_token)
-            }
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
index e8753c138..94038daa9 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
@@ -16,63 +16,48 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::personal_access_tokens::DeletePersonalAccessTokenRequest;
 use iggy_common::delete_personal_access_token::DeletePersonalAccessToken;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeletePersonalAccessToken {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_PERSONAL_ACCESS_TOKEN_CODE
-    }
+#[instrument(skip_all, name = "trace_delete_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_personal_access_token(
+    req: DeletePersonalAccessTokenRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: delete_personal_access_token, name: {}",
+        req.name.as_str()
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_delete_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = DeletePersonalAccessToken {
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-        let request =
-            ShardRequest::control_plane(ShardRequestPayload::DeletePersonalAccessTokenRequest {
-                user_id: session.get_user_id(),
-                command: self,
-            });
+    let request =
+        ShardRequest::control_plane(ShardRequestPayload::DeletePersonalAccessTokenRequest {
+            user_id: session.get_user_id(),
+            command,
+        });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeletePersonalAccessTokenResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeletePersonalAccessTokenResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeletePersonalAccessTokenResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeletePersonalAccessTokenResponse"),
     }
-}
 
-impl BinaryServerCommand for DeletePersonalAccessToken {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeletePersonalAccessToken(delete_personal_access_token) => {
-                Ok(delete_personal_access_token)
-            }
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs
index e4c3965f2..b04e1490c 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs
@@ -16,59 +16,46 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::HandlerResult;
 use crate::binary::handlers::personal_access_tokens::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::personal_access_tokens::{
+    GetPersonalAccessTokensResponse, PersonalAccessTokenResponse,
+};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_personal_access_tokens::GetPersonalAccessTokens;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetPersonalAccessTokens {
-    fn code(&self) -> u32 {
-        iggy_common::GET_PERSONAL_ACCESS_TOKENS_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        let personal_access_tokens = shard
-            .get_personal_access_tokens(session.get_user_id())
-            .error(|e: &IggyError| {
-                format!(
-                    "{COMPONENT} (error: {e}) - failed to get personal access tokens for user: {}",
-                    session.get_user_id()
-                )
-            })?;
-        let personal_access_tokens = mapper::map_personal_access_tokens(personal_access_tokens);
-        sender.send_ok_response(&personal_access_tokens).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for GetPersonalAccessTokens {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetPersonalAccessTokens(get_personal_access_tokens) => {
-                Ok(get_personal_access_tokens)
-            }
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+pub async fn handle_get_personal_access_tokens(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_personal_access_tokens");
+    shard.ensure_authenticated(session)?;
+    let personal_access_tokens = shard
+        .get_personal_access_tokens(session.get_user_id())
+        .error(|e: &IggyError| {
+            format!(
+                "{COMPONENT} (error: {e}) - failed to get personal access tokens for user: {}",
+                session.get_user_id()
+            )
+        })?;
+    let tokens: Vec<PersonalAccessTokenResponse> = personal_access_tokens
+        .iter()
+        .map(|pat| {
+            Ok(PersonalAccessTokenResponse {
+                name: WireName::new(pat.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+                expiry_at: pat.expiry_at.map_or(0, |e| e.as_micros()),
+            })
+        })
+        .collect::<Result<_, IggyError>>()?;
+    let response = GetPersonalAccessTokensResponse { tokens };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
index 3a7475d3b..2b32ff51c 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
@@ -15,60 +15,44 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-use crate::shard::IggyShard;
-use std::rc::Rc;
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::HandlerResult;
 use crate::binary::handlers::personal_access_tokens::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::personal_access_tokens::LoginWithPersonalAccessTokenRequest;
+use iggy_binary_protocol::responses::users::IdentityResponse;
 use iggy_common::login_with_personal_access_token::LoginWithPersonalAccessToken;
-use iggy_common::{IggyError, SenderKind};
-use secrecy::ExposeSecret;
+use iggy_common::{IggyError, SenderKind, Validatable};
+use secrecy::SecretString;
+use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for LoginWithPersonalAccessToken {
-    fn code(&self) -> u32 {
-        iggy_common::LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE
-    }
+#[instrument(skip_all, name = "trace_login_with_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_login_with_personal_access_token(
+    req: LoginWithPersonalAccessTokenRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: login_with_personal_access_token");
+    let token = req.token.as_str();
 
-    #[instrument(skip_all, name = "trace_login_with_personal_access_token", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        let token = self.token.expose_secret();
-        let user = shard
-            .login_with_personal_access_token(token, Some(session))
-            .error(|e: &IggyError| {
-                format!(
-                    "{COMPONENT} (error: {e}) - failed to login with personal access token, session: {session}",
-                )
-            })?;
-        let identity_info = mapper::map_identity_info(user.id);
-        sender.send_ok_response(&identity_info).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let command = LoginWithPersonalAccessToken {
+        token: SecretString::from(token),
+    };
+    command.validate()?;
 
-impl BinaryServerCommand for LoginWithPersonalAccessToken {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::LoginWithPersonalAccessToken(login_with_personal_access_token) => {
-                Ok(login_with_personal_access_token)
-            }
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let user = shard
+        .login_with_personal_access_token(token, Some(session))
+        .error(|e: &IggyError| {
+            format!(
+                "{COMPONENT} (error: {e}) - failed to login with personal access token, session: {session}",
+            )
+        })?;
+    let response = IdentityResponse { user_id: user.id };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/segments/delete_segments_handler.rs b/core/server/src/binary/handlers/segments/delete_segments_handler.rs
index c8aa5ddc1..cf55d875c 100644
--- a/core/server/src/binary/handlers/segments/delete_segments_handler.rs
+++ b/core/server/src/binary/handlers/segments/delete_segments_handler.rs
@@ -16,80 +16,61 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
-use iggy_common::delete_segments::DeleteSegments;
+use iggy_binary_protocol::requests::segments::DeleteSegmentsRequest;
 use iggy_common::sharding::IggyNamespace;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeleteSegments {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_SEGMENTS_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_delete_segments", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+#[instrument(skip_all, name = "trace_delete_segments", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_segments(
+    req: DeleteSegmentsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: delete_segments, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-        let partition_id = self.partition_id as usize;
-        let segments_count = self.segments_count;
+    let partition_id = req.partition_id as usize;
+    let segments_count = req.segments_count;
 
-        let partition = shard.resolve_partition_for_delete_segments(
-            session.get_user_id(),
-            &self.stream_id,
-            &self.topic_id,
-            partition_id,
-        )?;
+    let partition = shard.resolve_partition_for_delete_segments(
+        session.get_user_id(),
+        &stream_id,
+        &topic_id,
+        partition_id,
+    )?;
 
-        let namespace = IggyNamespace::new(
-            partition.stream_id,
-            partition.topic_id,
-            partition.partition_id,
-        );
-        let payload = ShardRequestPayload::DeleteSegments { segments_count };
-        let request = ShardRequest::data_plane(namespace, payload);
+    let namespace = IggyNamespace::new(
+        partition.stream_id,
+        partition.topic_id,
+        partition.partition_id,
+    );
+    let payload = ShardRequestPayload::DeleteSegments { segments_count };
+    let request = ShardRequest::data_plane(namespace, payload);
 
-        match shard.send_to_data_plane(request).await? {
-            ShardResponse::DeleteSegments {
-                deleted_segments,
-                deleted_messages,
-            } => {
-                shard.metrics.decrement_segments(deleted_segments as u32);
-                shard.metrics.decrement_messages(deleted_messages);
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeleteSegments"),
+    match shard.send_to_data_plane(request).await? {
+        ShardResponse::DeleteSegments {
+            deleted_segments,
+            deleted_messages,
+        } => {
+            shard.metrics.decrement_segments(deleted_segments as u32);
+            shard.metrics.decrement_messages(deleted_messages);
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeleteSegments"),
     }
-}
 
-impl BinaryServerCommand for DeleteSegments {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteSegments(delete_segments) => Ok(delete_segments),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/segments/mod.rs b/core/server/src/binary/handlers/segments/mod.rs
index 551c850b1..b1e454f5b 100644
--- a/core/server/src/binary/handlers/segments/mod.rs
+++ b/core/server/src/binary/handlers/segments/mod.rs
@@ -15,4 +15,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-mod delete_segments_handler;
+pub mod delete_segments_handler;
diff --git a/core/server/src/binary/handlers/streams/create_stream_handler.rs b/core/server/src/binary/handlers/streams/create_stream_handler.rs
index f1a4e5e66..15aa88888 100644
--- a/core/server/src/binary/handlers/streams/create_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/create_stream_handler.rs
@@ -16,64 +16,58 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::streams::CreateStreamRequest;
+use iggy_binary_protocol::responses::streams::StreamResponse;
 use iggy_common::create_stream::CreateStream;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreateStream {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_STREAM_CODE
-    }
+#[instrument(skip_all, name = "trace_create_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_stream(
+    req: CreateStreamRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: create_stream, name: {}",
+        req.name.as_str()
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_create_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = CreateStream {
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::CreateStreamRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::CreateStreamRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreateStreamResponse(data) => {
-                sender
-                    .send_ok_response(&mapper::map_stream_from_response(&data))
-                    .await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreateStreamResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreateStreamResponse(data) => {
+            let response = StreamResponse {
+                id: data.id,
+                created_at: data.created_at.into(),
+                topics_count: 0,
+                size_bytes: 0,
+                messages_count: 0,
+                name: WireName::new(data.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+            };
+            sender.send_ok_response(&response.to_bytes()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreateStreamResponse"),
     }
-}
 
-impl BinaryServerCommand for CreateStream {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreateStream(create_stream) => Ok(create_stream),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/streams/delete_stream_handler.rs b/core/server/src/binary/handlers/streams/delete_stream_handler.rs
index 0ff0d06ab..bb142fdd7 100644
--- a/core/server/src/binary/handlers/streams/delete_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/delete_stream_handler.rs
@@ -16,61 +16,45 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::streams::DeleteStreamRequest;
 use iggy_common::delete_stream::DeleteStream;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeleteStream {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_STREAM_CODE
-    }
+#[instrument(skip_all, name = "trace_delete_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_stream(
+    req: DeleteStreamRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: delete_stream, stream_id: {:?}",
+        req.stream_id
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_delete_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let command = DeleteStream { stream_id };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::DeleteStreamRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::DeleteStreamRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeleteStreamResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeleteStreamResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeleteStreamResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeleteStreamResponse"),
     }
-}
 
-impl BinaryServerCommand for DeleteStream {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteStream(delete_stream) => Ok(delete_stream),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/streams/get_stream_handler.rs b/core/server/src/binary/handlers/streams/get_stream_handler.rs
index 1a29a226a..fd451a71d 100644
--- a/core/server/src/binary/handlers/streams/get_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/get_stream_handler.rs
@@ -16,57 +16,107 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
+use crate::metadata::{StreamMeta, TopicMeta};
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::streams::GetStreamRequest;
+use iggy_binary_protocol::responses::streams::StreamResponse;
+use iggy_binary_protocol::responses::streams::get_stream::{GetStreamResponse, TopicHeader};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_stream::GetStream;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetStream {
-    fn code(&self) -> u32 {
-        iggy_common::GET_STREAM_CODE
-    }
+pub async fn handle_get_stream(
+    req: GetStreamRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: get_stream, stream_id: {:?}",
+        req.stream_id
+    );
+    shard.ensure_authenticated(session)?;
+
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+
+    let Some(stream) = shard
+        .metadata
+        .query_stream(session.get_user_id(), &stream_id)?
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let response = build_get_stream_response(&stream)?;
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
+}
 
-        let Some(stream) = shard
-            .metadata
-            .query_stream(session.get_user_id(), &self.stream_id)?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
+pub(crate) fn compute_stream_stats(stream: &StreamMeta) -> (u64, u64) {
+    let mut size = 0u64;
+    let mut messages = 0u64;
+    for (_, topic) in stream.topics.iter() {
+        for partition in topic.partitions.iter() {
+            size += partition.stats.size_bytes_inconsistent();
+            messages += partition.stats.messages_count_inconsistent();
+        }
+    }
+    (size, messages)
+}
 
-        let response = mapper::map_stream(&stream);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
+fn compute_topic_stats(topic: &TopicMeta) -> (u64, u64) {
+    let mut size = 0u64;
+    let mut messages = 0u64;
+    for partition in topic.partitions.iter() {
+        size += partition.stats.size_bytes_inconsistent();
+        messages += partition.stats.messages_count_inconsistent();
     }
+    (size, messages)
+}
+
+pub(crate) fn build_topic_header(topic: &TopicMeta) -> Result<TopicHeader, IggyError> {
+    let (size, messages) = compute_topic_stats(topic);
+    Ok(TopicHeader {
+        id: topic.id as u32,
+        created_at: topic.created_at.into(),
+        partitions_count: topic.partitions.len() as u32,
+        message_expiry: topic.message_expiry.into(),
+        compression_algorithm: topic.compression_algorithm.as_code(),
+        max_topic_size: topic.max_topic_size.into(),
+        replication_factor: topic.replication_factor,
+        size_bytes: size,
+        messages_count: messages,
+        name: WireName::new(topic.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+    })
 }
 
-impl BinaryServerCommand for GetStream {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetStream(get_stream) => Ok(get_stream),
-            _ => Err(IggyError::InvalidCommand),
+fn build_get_stream_response(stream: &StreamMeta) -> Result<GetStreamResponse, IggyError> {
+    let mut topic_ids: Vec<_> = stream.topics.iter().map(|(k, _)| k).collect();
+    topic_ids.sort_unstable();
+
+    let (total_size, total_messages) = compute_stream_stats(stream);
+
+    let mut topics = Vec::with_capacity(topic_ids.len());
+    for &topic_id in &topic_ids {
+        if let Some(topic) = stream.topics.get(topic_id) {
+            topics.push(build_topic_header(topic)?);
         }
     }
+
+    Ok(GetStreamResponse {
+        stream: StreamResponse {
+            id: stream.id as u32,
+            created_at: stream.created_at.into(),
+            topics_count: topic_ids.len() as u32,
+            size_bytes: total_size,
+            messages_count: total_messages,
+            name: WireName::new(stream.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+        },
+        topics,
+    })
 }
diff --git a/core/server/src/binary/handlers/streams/get_streams_handler.rs b/core/server/src/binary/handlers/streams/get_streams_handler.rs
index 8cea11ba7..ca5524378 100644
--- a/core/server/src/binary/handlers/streams/get_streams_handler.rs
+++ b/core/server/src/binary/handlers/streams/get_streams_handler.rs
@@ -16,50 +16,48 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use super::get_stream_handler::compute_stream_stats;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::streams::StreamResponse;
+use iggy_binary_protocol::responses::streams::get_streams::GetStreamsResponse;
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_streams::GetStreams;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetStreams {
-    fn code(&self) -> u32 {
-        iggy_common::GET_STREAMS_CODE
-    }
+pub async fn handle_get_streams(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_streams");
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let streams = shard.metadata.query_streams(session.get_user_id())?;
 
-        let streams = shard.metadata.query_streams(session.get_user_id())?;
-        let response = mapper::map_streams(&streams);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let mut sorted: Vec<_> = streams.iter().collect();
+    sorted.sort_by_key(|s| s.id);
 
-impl BinaryServerCommand for GetStreams {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetStreams(get_streams) => Ok(get_streams),
-            _ => Err(IggyError::InvalidCommand),
-        }
+    let mut wire_streams = Vec::with_capacity(sorted.len());
+    for stream in sorted {
+        let (total_size, total_messages) = compute_stream_stats(stream);
+        wire_streams.push(StreamResponse {
+            id: stream.id as u32,
+            created_at: stream.created_at.into(),
+            topics_count: stream.topics.len() as u32,
+            size_bytes: total_size,
+            messages_count: total_messages,
+            name: WireName::new(stream.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+        });
     }
+
+    let response = GetStreamsResponse {
+        streams: wire_streams,
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/streams/purge_stream_handler.rs b/core/server/src/binary/handlers/streams/purge_stream_handler.rs
index 6346fba9e..7bffb6ad4 100644
--- a/core/server/src/binary/handlers/streams/purge_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/purge_stream_handler.rs
@@ -16,61 +16,45 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::streams::PurgeStreamRequest;
 use iggy_common::purge_stream::PurgeStream;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for PurgeStream {
-    fn code(&self) -> u32 {
-        iggy_common::PURGE_STREAM_CODE
-    }
+#[instrument(skip_all, name = "trace_purge_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_purge_stream(
+    req: PurgeStreamRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: purge_stream, stream_id: {:?}",
+        req.stream_id
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_purge_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let command = PurgeStream { stream_id };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::PurgeStreamRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::PurgeStreamRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::PurgeStreamResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected PurgeStreamResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::PurgeStreamResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected PurgeStreamResponse"),
     }
-}
 
-impl BinaryServerCommand for PurgeStream {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::PurgeStream(purge_stream) => Ok(purge_stream),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/streams/update_stream_handler.rs b/core/server/src/binary/handlers/streams/update_stream_handler.rs
index 2deb7fcd2..d72e383b6 100644
--- a/core/server/src/binary/handlers/streams/update_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/update_stream_handler.rs
@@ -16,61 +16,49 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::streams::UpdateStreamRequest;
 use iggy_common::update_stream::UpdateStream;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for UpdateStream {
-    fn code(&self) -> u32 {
-        iggy_common::UPDATE_STREAM_CODE
-    }
+#[instrument(skip_all, name = "trace_update_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_update_stream(
+    req: UpdateStreamRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: update_stream, stream_id: {:?}",
+        req.stream_id
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_update_stream", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let command = UpdateStream {
+        stream_id,
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::UpdateStreamRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::UpdateStreamRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::UpdateStreamResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected UpdateStreamResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::UpdateStreamResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected UpdateStreamResponse"),
     }
-}
 
-impl BinaryServerCommand for UpdateStream {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::UpdateStream(update_stream) => Ok(update_stream),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/system/get_client_handler.rs b/core/server/src/binary/handlers/system/get_client_handler.rs
index 51bee9e2f..594cf8373 100644
--- a/core/server/src/binary/handlers/system/get_client_handler.rs
+++ b/core/server/src/binary/handlers/system/get_client_handler.rs
@@ -16,55 +16,40 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use super::get_me_handler::build_client_details_response;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::system::GetClientRequest;
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_client::GetClient;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetClient {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CLIENT_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_get_client(session.get_user_id())?;
-
-        let Some(client) = shard.get_client(self.client_id) else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let bytes = mapper::map_client(&client);
+pub async fn handle_get_client(
+    req: GetClientRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: get_client, client_id: {}",
+        req.client_id
+    );
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_get_client(session.get_user_id())?;
 
-        sender.send_ok_response(&bytes).await?;
-        Ok(HandlerResult::Finished)
+    if req.client_id == 0 {
+        return Err(IggyError::InvalidClientId);
     }
-}
 
-impl BinaryServerCommand for GetClient {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetClient(get_client) => Ok(get_client),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let Some(client) = shard.get_client(req.client_id) else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
+
+    let response = build_client_details_response(&client);
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/system/get_clients_handler.rs b/core/server/src/binary/handlers/system/get_clients_handler.rs
index ad5ccdc2d..cceda4622 100644
--- a/core/server/src/binary/handlers/system/get_clients_handler.rs
+++ b/core/server/src/binary/handlers/system/get_clients_handler.rs
@@ -16,51 +16,30 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use super::get_me_handler::build_client_response;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::clients::GetClientsResponse;
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_clients::GetClients;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetClients {
-    fn code(&self) -> u32 {
-        iggy_common::GET_CLIENTS_CODE
-    }
+pub async fn handle_get_clients(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_clients");
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_get_clients(session.get_user_id())?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_get_clients(session.get_user_id())?;
-
-        let clients = shard.get_clients();
-        let clients = mapper::map_clients(clients).await;
-        sender.send_ok_response(&clients).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for GetClients {
-    async fn from_sender(
-        sender: &mut SenderKind,
-        code: u32,
-        length: u32,
-    ) -> Result<Self, IggyError> {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetClients(get_clients) => Ok(get_clients),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let clients = shard.get_clients();
+    let response = GetClientsResponse {
+        clients: clients.iter().map(build_client_response).collect(),
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/system/get_me_handler.rs b/core/server/src/binary/handlers/system/get_me_handler.rs
index 6913a7529..0d635bf6a 100644
--- a/core/server/src/binary/handlers/system/get_me_handler.rs
+++ b/core/server/src/binary/handlers/system/get_me_handler.rs
@@ -16,50 +16,63 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
+use crate::streaming::clients::client_manager::Client;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::clients::{
+    ClientDetailsResponse, ClientResponse, ConsumerGroupInfoResponse,
+};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_me::GetMe;
 use std::rc::Rc;
 
-impl ServerCommandHandler for GetMe {
-    fn code(&self) -> u32 {
-        iggy_common::GET_ME_CODE
-    }
+pub async fn handle_get_me(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    shard.ensure_authenticated(session)?;
+    let Some(client) = shard.get_client(session.client_id) else {
+        return Err(IggyError::ClientNotFound(session.client_id));
+    };
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        shard.ensure_authenticated(session)?;
-        let Some(client) = shard.get_client(session.client_id) else {
-            return Err(IggyError::ClientNotFound(session.client_id));
-        };
+    let response = build_client_details_response(&client);
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
+}
 
-        let bytes = mapper::map_client(&client);
+pub(crate) fn build_client_details_response(client: &Client) -> ClientDetailsResponse {
+    ClientDetailsResponse {
+        client: build_client_response(client),
+        consumer_groups: client
+            .consumer_groups
+            .iter()
+            .map(|cg| ConsumerGroupInfoResponse {
+                stream_id: cg.stream_id,
+                topic_id: cg.topic_id,
+                group_id: cg.group_id,
+            })
+            .collect(),
+    }
+}
 
-        sender.send_ok_response(&bytes).await?;
-        Ok(HandlerResult::Finished)
+pub(crate) fn build_client_response(client: &Client) -> ClientResponse {
+    ClientResponse {
+        client_id: client.session.client_id,
+        user_id: client.user_id.unwrap_or(u32::MAX),
+        transport: transport_to_u8(&client.transport),
+        address: client.session.ip_address.to_string(),
+        consumer_groups_count: client.consumer_groups.len() as u32,
     }
 }
 
-impl BinaryServerCommand for GetMe {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetMe(get_me) => Ok(get_me),
-            _ => Err(IggyError::InvalidCommand),
-        }
+pub(crate) fn transport_to_u8(transport: &iggy_common::TransportProtocol) -> u8 {
+    match transport {
+        iggy_common::TransportProtocol::Tcp => 1,
+        iggy_common::TransportProtocol::Quic => 2,
+        iggy_common::TransportProtocol::Http => 3,
+        iggy_common::TransportProtocol::WebSocket => 4,
     }
 }
diff --git a/core/server/src/binary/handlers/system/get_snapshot_handler.rs b/core/server/src/binary/handlers/system/get_snapshot_handler.rs
index 082ef8b70..4eabf63ac 100644
--- a/core/server/src/binary/handlers/system/get_snapshot_handler.rs
+++ b/core/server/src/binary/handlers/system/get_snapshot_handler.rs
@@ -16,51 +16,37 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use bytes::Bytes;
-use iggy_common::IggyError;
-use iggy_common::SenderKind;
-use iggy_common::get_snapshot::GetSnapshot;
+use iggy_binary_protocol::requests::system::GetSnapshotRequest;
+use iggy_common::{IggyError, SenderKind, SnapshotCompression, SystemSnapshotType};
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetSnapshot {
-    fn code(&self) -> u32 {
-        iggy_common::GET_SNAPSHOT_FILE_CODE
-    }
+pub async fn handle_get_snapshot(
+    req: GetSnapshotRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_snapshot");
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let compression = SnapshotCompression::from_code(req.compression)?;
+    let snapshot_types: Vec<SystemSnapshotType> = req
+        .snapshot_types
+        .iter()
+        .map(|&code| SystemSnapshotType::from_code(code))
+        .collect::<Result<_, _>>()?;
 
-        let snapshot = shard
-            .get_snapshot(self.compression, &self.snapshot_types)
-            .await?;
-        let bytes = Bytes::copy_from_slice(&snapshot.0);
-        sender.send_ok_response(&bytes).await?;
-        Ok(HandlerResult::Finished)
+    if snapshot_types.contains(&SystemSnapshotType::All) && snapshot_types.len() > 1 {
+        return Err(IggyError::InvalidCommand);
     }
-}
 
-impl BinaryServerCommand for GetSnapshot {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetSnapshot(get_snapshot) => Ok(get_snapshot),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let snapshot = shard.get_snapshot(compression, &snapshot_types).await?;
+    let bytes = Bytes::copy_from_slice(&snapshot.0);
+    sender.send_ok_response(&bytes).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/system/get_stats_handler.rs b/core/server/src/binary/handlers/system/get_stats_handler.rs
index 6ec310920..26aa2ec23 100644
--- a/core/server/src/binary/handlers/system/get_stats_handler.rs
+++ b/core/server/src/binary/handlers/system/get_stats_handler.rs
@@ -16,62 +16,79 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::system::get_stats::{CacheMetricEntry, StatsResponse};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_stats::GetStats;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetStats {
-    fn code(&self) -> u32 {
-        iggy_common::GET_STATS_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_get_stats(session.get_user_id())?;
+pub async fn handle_get_stats(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_stats");
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_get_stats(session.get_user_id())?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::GetStats {
-            user_id: session.get_user_id(),
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::GetStats {
+        user_id: session.get_user_id(),
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::GetStatsResponse(stats) => {
-                sender.send_ok_response(&mapper::map_stats(&stats)).await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected GetStatsResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::GetStatsResponse(stats) => {
+            let response = StatsResponse {
+                process_id: stats.process_id,
+                cpu_usage: stats.cpu_usage,
+                total_cpu_usage: stats.total_cpu_usage,
+                memory_usage: stats.memory_usage.as_bytes_u64(),
+                total_memory: stats.total_memory.as_bytes_u64(),
+                available_memory: stats.available_memory.as_bytes_u64(),
+                run_time: stats.run_time.into(),
+                start_time: stats.start_time.into(),
+                read_bytes: stats.read_bytes.as_bytes_u64(),
+                written_bytes: stats.written_bytes.as_bytes_u64(),
+                messages_size_bytes: stats.messages_size_bytes.as_bytes_u64(),
+                streams_count: stats.streams_count,
+                topics_count: stats.topics_count,
+                partitions_count: stats.partitions_count,
+                segments_count: stats.segments_count,
+                messages_count: stats.messages_count,
+                clients_count: stats.clients_count,
+                consumer_groups_count: stats.consumer_groups_count,
+                hostname: stats.hostname,
+                os_name: stats.os_name,
+                os_version: stats.os_version,
+                kernel_version: stats.kernel_version,
+                iggy_server_version: stats.iggy_server_version,
+                iggy_server_semver: stats.iggy_server_semver,
+                cache_metrics: stats
+                    .cache_metrics
+                    .iter()
+                    .map(|(key, metrics)| CacheMetricEntry {
+                        stream_id: key.stream_id,
+                        topic_id: key.topic_id,
+                        partition_id: key.partition_id,
+                        hits: metrics.hits,
+                        misses: metrics.misses,
+                        hit_ratio: metrics.hit_ratio,
+                    })
+                    .collect(),
+                threads_count: stats.threads_count,
+                free_disk_space: stats.free_disk_space.as_bytes_u64(),
+                total_disk_space: stats.total_disk_space.as_bytes_u64(),
+            };
+            sender.send_ok_response(&response.to_bytes()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected GetStatsResponse"),
     }
-}
 
-impl BinaryServerCommand for GetStats {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        let server_command = receive_and_validate(sender, code, length).await?;
-        match server_command {
-            ServerCommand::GetStats(get_stats) => Ok(get_stats),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/system/ping_handler.rs b/core/server/src/binary/handlers/system/ping_handler.rs
index 746de588f..6633cd0b0 100644
--- a/core/server/src/binary/handlers/system/ping_handler.rs
+++ b/core/server/src/binary/handlers/system/ping_handler.rs
@@ -16,49 +16,27 @@
  * under the License.
  */
 
-use crate::binary::command::{BinaryServerCommand, HandlerResult, ServerCommandHandler};
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use iggy_common::IggyError;
 use iggy_common::IggyTimestamp;
 use iggy_common::SenderKind;
-use iggy_common::ping::Ping;
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for Ping {
-    fn code(&self) -> u32 {
-        iggy_common::PING_CODE
+pub async fn handle_ping(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: ping");
+    if let Some(mut client) = shard.client_manager.try_get_client_mut(session.client_id) {
+        let now = IggyTimestamp::now();
+        client.last_heartbeat = now;
+        debug!("Updated last heartbeat to: {now} for session: {session}");
     }
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        if let Some(mut client) = shard.client_manager.try_get_client_mut(session.client_id) {
-            let now = IggyTimestamp::now();
-            client.last_heartbeat = now;
-            debug!("Updated last heartbeat to: {now} for session: {session}");
-        }
-
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
-    }
-}
-
-impl BinaryServerCommand for Ping {
-    async fn from_sender(
-        _sender: &mut SenderKind,
-        _length: u32,
-        _code: u32,
-    ) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        Ok(Ping {})
-    }
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/topics/create_topic_handler.rs b/core/server/src/binary/handlers/topics/create_topic_handler.rs
index dbadbb07f..10c142d9a 100644
--- a/core/server/src/binary/handlers/topics/create_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/create_topic_handler.rs
@@ -16,63 +16,101 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use bytes::BytesMut;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::topics::CreateTopicRequest;
+use iggy_binary_protocol::responses::streams::get_stream::TopicHeader;
+use iggy_binary_protocol::responses::topics::get_topic::PartitionResponse;
 use iggy_common::create_topic::CreateTopic;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{
+    CompressionAlgorithm, IggyError, IggyExpiry, MaxTopicSize, SenderKind, Validatable,
+};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreateTopic {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_TOPIC_CODE
-    }
+#[instrument(skip_all, name = "trace_create_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_topic(
+    req: CreateTopicRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    debug!(
+        "session: {session}, command: create_topic, stream_id: {stream_id}, name: {}",
+        req.name.as_str()
+    );
+    shard.ensure_authenticated(session)?;
+
+    let command = CreateTopic {
+        stream_id,
+        partitions_count: req.partitions_count,
+        compression_algorithm: CompressionAlgorithm::from_code(req.compression_algorithm)?,
+        message_expiry: IggyExpiry::from(req.message_expiry),
+        max_topic_size: MaxTopicSize::from(req.max_topic_size),
+        replication_factor: if req.replication_factor == 0 {
+            None
+        } else {
+            Some(req.replication_factor)
+        },
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-    #[instrument(skip_all, name = "trace_create_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let request = ShardRequest::control_plane(ShardRequestPayload::CreateTopicRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::CreateTopicRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreateTopicResponse(data) => {
+            let header = TopicHeader {
+                id: data.id,
+                created_at: data.created_at.into(),
+                partitions_count: data.partitions.len() as u32,
+                message_expiry: data.message_expiry.into(),
+                compression_algorithm: data.compression_algorithm.as_code(),
+                max_topic_size: data.max_topic_size.into(),
+                replication_factor: data.replication_factor,
+                size_bytes: 0,
+                messages_count: 0,
+                name: WireName::new(data.name.as_ref()).map_err(|_| IggyError::InvalidCommand)?,
+            };
+            let partitions: Vec<PartitionResponse> = data
+                .partitions
+                .iter()
+                .map(|p| PartitionResponse {
+                    id: p.id as u32,
+                    created_at: p.created_at.into(),
+                    segments_count: 0,
+                    current_offset: 0,
+                    size_bytes: 0,
+                    messages_count: 0,
+                })
+                .collect();
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreateTopicResponse(data) => {
-                sender
-                    .send_ok_response(&mapper::map_topic_from_response(&data))
-                    .await?;
+            let mut buf = BytesMut::with_capacity(
+                header.encoded_size()
+                    + partitions
+                        .iter()
+                        .map(WireEncode::encoded_size)
+                        .sum::<usize>(),
+            );
+            header.encode(&mut buf);
+            for partition in &partitions {
+                partition.encode(&mut buf);
             }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreateTopicResponse"),
+            sender.send_ok_response(&buf.freeze()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreateTopicResponse"),
     }
-}
 
-impl BinaryServerCommand for CreateTopic {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreateTopic(create_topic) => Ok(create_topic),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/topics/delete_topic_handler.rs b/core/server/src/binary/handlers/topics/delete_topic_handler.rs
index 6305be292..53cbc9d1c 100644
--- a/core/server/src/binary/handlers/topics/delete_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/delete_topic_handler.rs
@@ -16,60 +16,48 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::topics::DeleteTopicRequest;
 use iggy_common::delete_topic::DeleteTopic;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeleteTopic {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_TOPIC_CODE
-    }
+#[instrument(skip_all, name = "trace_delete_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_topic(
+    req: DeleteTopicRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: delete_topic, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_delete_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = DeleteTopic {
+        stream_id,
+        topic_id,
+    };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::DeleteTopicRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::DeleteTopicRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeleteTopicResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeleteTopicResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeleteTopicResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeleteTopicResponse"),
     }
-}
 
-impl BinaryServerCommand for DeleteTopic {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteTopic(delete_topic) => Ok(delete_topic),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/topics/get_topic_handler.rs b/core/server/src/binary/handlers/topics/get_topic_handler.rs
index 052cebb2c..5e44a4b4a 100644
--- a/core/server/src/binary/handlers/topics/get_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/get_topic_handler.rs
@@ -16,57 +16,61 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
+use crate::binary::handlers::streams::get_stream_handler::build_topic_header;
+use crate::metadata::TopicMeta;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
-use iggy_common::IggyError;
-use iggy_common::SenderKind;
-use iggy_common::get_topic::GetTopic;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::topics::GetTopicRequest;
+use iggy_binary_protocol::responses::topics::get_topic::{GetTopicResponse, PartitionResponse};
+use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetTopic {
-    fn code(&self) -> u32 {
-        iggy_common::GET_TOPIC_CODE
-    }
+pub async fn handle_get_topic(
+    req: GetTopicRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!("session: {session}, command: get_topic, stream_id: {stream_id}, topic_id: {topic_id}");
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let Some(topic) = shard
+        .metadata
+        .query_topic(session.get_user_id(), &stream_id, &topic_id)?
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-        let Some(topic) =
-            shard
-                .metadata
-                .query_topic(session.get_user_id(), &self.stream_id, &self.topic_id)?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let response = mapper::map_topic(&topic);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
+    let response = build_get_topic_response(&topic)?;
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
 
-impl BinaryServerCommand for GetTopic {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetTopic(get_topic) => Ok(get_topic),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+fn build_get_topic_response(topic: &TopicMeta) -> Result<GetTopicResponse, IggyError> {
+    let header = build_topic_header(topic)?;
+
+    let partitions: Vec<PartitionResponse> = topic
+        .partitions
+        .iter()
+        .enumerate()
+        .map(|(partition_id, partition)| PartitionResponse {
+            id: partition_id as u32,
+            created_at: partition.created_at.into(),
+            segments_count: partition.stats.segments_count_inconsistent(),
+            current_offset: partition.stats.current_offset(),
+            size_bytes: partition.stats.size_bytes_inconsistent(),
+            messages_count: partition.stats.messages_count_inconsistent(),
+        })
+        .collect();
+
+    Ok(GetTopicResponse {
+        topic: header,
+        partitions,
+    })
 }
diff --git a/core/server/src/binary/handlers/topics/get_topics_handler.rs b/core/server/src/binary/handlers/topics/get_topics_handler.rs
index 41987f3f4..e8d0a3d30 100644
--- a/core/server/src/binary/handlers/topics/get_topics_handler.rs
+++ b/core/server/src/binary/handlers/topics/get_topics_handler.rs
@@ -16,56 +16,46 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
+use crate::binary::handlers::streams::get_stream_handler::build_topic_header;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
-use iggy_common::IggyError;
-use iggy_common::SenderKind;
-use iggy_common::get_topics::GetTopics;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::topics::GetTopicsRequest;
+use iggy_binary_protocol::responses::topics::get_topics::GetTopicsResponse;
+use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetTopics {
-    fn code(&self) -> u32 {
-        iggy_common::GET_TOPICS_CODE
-    }
+pub async fn handle_get_topics(
+    req: GetTopicsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    debug!("session: {session}, command: get_topics, stream_id: {stream_id}");
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let Some(topics) = shard
+        .metadata
+        .query_topics(session.get_user_id(), &stream_id)?
+    else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-        let Some(topics) = shard
-            .metadata
-            .query_topics(session.get_user_id(), &self.stream_id)?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
+    let mut sorted: Vec<_> = topics.iter().collect();
+    sorted.sort_by_key(|t| t.id);
 
-        let response = mapper::map_topics(&topics);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
+    let mut wire_topics = Vec::with_capacity(sorted.len());
+    for topic in sorted {
+        wire_topics.push(build_topic_header(topic)?);
     }
-}
 
-impl BinaryServerCommand for GetTopics {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetTopics(get_topics) => Ok(get_topics),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let response = GetTopicsResponse {
+        topics: wire_topics,
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/topics/purge_topic_handler.rs b/core/server/src/binary/handlers/topics/purge_topic_handler.rs
index 1c1281559..579c3b29e 100644
--- a/core/server/src/binary/handlers/topics/purge_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/purge_topic_handler.rs
@@ -16,60 +16,48 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::topics::PurgeTopicRequest;
 use iggy_common::purge_topic::PurgeTopic;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for PurgeTopic {
-    fn code(&self) -> u32 {
-        iggy_common::PURGE_TOPIC_CODE
-    }
+#[instrument(skip_all, name = "trace_purge_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_purge_topic(
+    req: PurgeTopicRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: purge_topic, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_purge_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = PurgeTopic {
+        stream_id,
+        topic_id,
+    };
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::PurgeTopicRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::PurgeTopicRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::PurgeTopicResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected PurgeTopicResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::PurgeTopicResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected PurgeTopicResponse"),
     }
-}
 
-impl BinaryServerCommand for PurgeTopic {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::PurgeTopic(purge_topic) => Ok(purge_topic),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/topics/update_topic_handler.rs b/core/server/src/binary/handlers/topics/update_topic_handler.rs
index cb481e5c8..fa914f23d 100644
--- a/core/server/src/binary/handlers/topics/update_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/update_topic_handler.rs
@@ -16,60 +16,60 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::topics::UpdateTopicRequest;
 use iggy_common::update_topic::UpdateTopic;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{
+    CompressionAlgorithm, IggyError, IggyExpiry, MaxTopicSize, SenderKind, Validatable,
+};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for UpdateTopic {
-    fn code(&self) -> u32 {
-        iggy_common::UPDATE_TOPIC_CODE
-    }
+#[instrument(skip_all, name = "trace_update_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_update_topic(
+    req: UpdateTopicRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    let stream_id = wire_id_to_identifier(&req.stream_id)?;
+    let topic_id = wire_id_to_identifier(&req.topic_id)?;
+    debug!(
+        "session: {session}, command: update_topic, stream_id: {stream_id}, topic_id: {topic_id}"
+    );
+    shard.ensure_authenticated(session)?;
 
-    #[instrument(skip_all, name = "trace_update_topic", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id, iggy_stream_id = self.stream_id.as_string(), iggy_topic_id = self.topic_id.as_string()))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let command = UpdateTopic {
+        stream_id,
+        topic_id,
+        compression_algorithm: CompressionAlgorithm::from_code(req.compression_algorithm)?,
+        message_expiry: IggyExpiry::from(req.message_expiry),
+        max_topic_size: MaxTopicSize::from(req.max_topic_size),
+        replication_factor: if req.replication_factor == 0 {
+            None
+        } else {
+            Some(req.replication_factor)
+        },
+        name: req.name.to_string(),
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::UpdateTopicRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::UpdateTopicRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::UpdateTopicResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected UpdateTopicResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::UpdateTopicResponse => {
+            sender.send_empty_ok_response().await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected UpdateTopicResponse"),
     }
-}
 
-impl BinaryServerCommand for UpdateTopic {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::UpdateTopic(update_topic) => Ok(update_topic),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/change_password_handler.rs b/core/server/src/binary/handlers/users/change_password_handler.rs
index 9bf1f52b3..99f3b5af4 100644
--- a/core/server/src/binary/handlers/users/change_password_handler.rs
+++ b/core/server/src/binary/handlers/users/change_password_handler.rs
@@ -16,65 +16,57 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::users::ChangePasswordRequest;
 use iggy_common::change_password::ChangePassword;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, Validatable};
+use secrecy::SecretString;
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for ChangePassword {
-    fn code(&self) -> u32 {
-        iggy_common::CHANGE_PASSWORD_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_change_password", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+#[instrument(skip_all, name = "trace_change_password", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_change_password(
+    req: ChangePasswordRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: change_password, user_id: {:?}",
+        req.user_id
+    );
+    shard.ensure_authenticated(session)?;
 
-        let target_user = shard.get_user(&self.user_id)?;
-        if target_user.id != session.get_user_id() {
-            shard.metadata.perm_change_password(session.get_user_id())?;
-        }
+    let user_id = wire_id_to_identifier(&req.user_id)?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::ChangePasswordRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let target_user = shard.get_user(&user_id)?;
+    if target_user.id != session.get_user_id() {
+        shard.metadata.perm_change_password(session.get_user_id())?;
+    }
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::ChangePasswordResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected ChangePasswordResponse"),
-        }
+    let command = ChangePassword {
+        user_id,
+        current_password: SecretString::from(req.current_password),
+        new_password: SecretString::from(req.new_password),
+    };
+    command.validate()?;
 
-        Ok(HandlerResult::Finished)
-    }
-}
+    let request = ShardRequest::control_plane(ShardRequestPayload::ChangePasswordRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-impl BinaryServerCommand for ChangePassword {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::ChangePassword(change_password) => Ok(change_password),
-            _ => Err(IggyError::InvalidCommand),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::ChangePasswordResponse => {
+            sender.send_empty_ok_response().await?;
         }
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected ChangePasswordResponse"),
     }
+
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/create_user_handler.rs b/core/server/src/binary/handlers/users/create_user_handler.rs
index f4994d6f5..68990933d 100644
--- a/core/server/src/binary/handlers/users/create_user_handler.rs
+++ b/core/server/src/binary/handlers/users/create_user_handler.rs
@@ -16,63 +16,70 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
+use crate::binary::dispatch::{
+    HandlerResult, domain_permissions_to_wire, wire_permissions_to_permissions,
 };
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::users::CreateUserRequest;
+use iggy_binary_protocol::responses::users::{UserDetailsResponse, UserResponse};
 use iggy_common::create_user::CreateUser;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, UserStatus, Validatable};
+use secrecy::SecretString;
 use std::rc::Rc;
-use tracing::debug;
-use tracing::instrument;
+use tracing::{debug, instrument};
 
-impl ServerCommandHandler for CreateUser {
-    fn code(&self) -> u32 {
-        iggy_common::CREATE_USER_CODE
-    }
+#[instrument(skip_all, name = "trace_create_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_create_user(
+    req: CreateUserRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: create_user, username: {}",
+        req.username.as_str()
+    );
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_create_user(session.get_user_id())?;
 
-    #[instrument(skip_all, name = "trace_create_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_create_user(session.get_user_id())?;
+    let command = CreateUser {
+        username: req.username.to_string(),
+        password: SecretString::from(req.password),
+        status: UserStatus::from_code(req.status)?,
+        permissions: req
+            .permissions
+            .as_ref()
+            .map(wire_permissions_to_permissions),
+    };
+    command.validate()?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::CreateUserRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let request = ShardRequest::control_plane(ShardRequestPayload::CreateUserRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::CreateUserResponse(user) => {
-                sender.send_ok_response(&mapper::map_user(&user)).await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected CreateUserResponse"),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::CreateUserResponse(user) => {
+            let response = UserDetailsResponse {
+                user: UserResponse {
+                    id: user.id,
+                    created_at: user.created_at.as_micros(),
+                    status: user.status.as_code(),
+                    username: WireName::new(&user.username)
+                        .map_err(|_| IggyError::InvalidCommand)?,
+                },
+                permissions: user.permissions.as_ref().map(domain_permissions_to_wire),
+            };
+            sender.send_ok_response(&response.to_bytes()).await?;
         }
-
-        Ok(HandlerResult::Finished)
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected CreateUserResponse"),
     }
-}
 
-impl BinaryServerCommand for CreateUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::CreateUser(create_user) => Ok(create_user),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/delete_user_handler.rs b/core/server/src/binary/handlers/users/delete_user_handler.rs
index 7164aab70..db8358f77 100644
--- a/core/server/src/binary/handlers/users/delete_user_handler.rs
+++ b/core/server/src/binary/handlers/users/delete_user_handler.rs
@@ -16,61 +16,47 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::users::DeleteUserRequest;
 use iggy_common::delete_user::DeleteUser;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for DeleteUser {
-    fn code(&self) -> u32 {
-        iggy_common::DELETE_USER_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_delete_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_delete_user(session.get_user_id())?;
+#[instrument(skip_all, name = "trace_delete_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_delete_user(
+    req: DeleteUserRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: delete_user, user_id: {:?}",
+        req.user_id
+    );
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_delete_user(session.get_user_id())?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::DeleteUserRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let user_id = wire_id_to_identifier(&req.user_id)?;
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::DeleteUserResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected DeleteUserResponse"),
-        }
+    let command = DeleteUser { user_id };
 
-        Ok(HandlerResult::Finished)
-    }
-}
+    let request = ShardRequest::control_plane(ShardRequestPayload::DeleteUserRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-impl BinaryServerCommand for DeleteUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::DeleteUser(delete_user) => Ok(delete_user),
-            _ => Err(IggyError::InvalidCommand),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::DeleteUserResponse(_) => {
+            sender.send_empty_ok_response().await?;
         }
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected DeleteUserResponse"),
     }
+
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/get_user_handler.rs b/core/server/src/binary/handlers/users/get_user_handler.rs
index 5a40a1171..66251e46c 100644
--- a/core/server/src/binary/handlers/users/get_user_handler.rs
+++ b/core/server/src/binary/handlers/users/get_user_handler.rs
@@ -16,57 +16,47 @@
  * under the License.
  */
 
-use std::rc::Rc;
-
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::{HandlerResult, domain_permissions_to_wire, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::users::GetUserRequest;
+use iggy_binary_protocol::responses::users::{UserDetailsResponse, UserResponse};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_user::GetUser;
+use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetUser {
-    fn code(&self) -> u32 {
-        iggy_common::GET_USER_CODE
-    }
+pub async fn handle_get_user(
+    req: GetUserRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: get_user, user_id: {:?}",
+        req.user_id
+    );
+    shard.ensure_authenticated(session)?;
 
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
+    let user_id = wire_id_to_identifier(&req.user_id)?;
 
-        let Some(user) = shard
-            .metadata
-            .query_user(session.get_user_id(), &self.user_id)?
-        else {
-            sender.send_empty_ok_response().await?;
-            return Ok(HandlerResult::Finished);
-        };
-
-        let bytes = mapper::map_user_meta(&user);
-        sender.send_ok_response(&bytes).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    let Some(user) = shard.metadata.query_user(session.get_user_id(), &user_id)? else {
+        sender.send_empty_ok_response().await?;
+        return Ok(HandlerResult::Finished);
+    };
 
-impl BinaryServerCommand for GetUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetUser(get_user) => Ok(get_user),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let response = UserDetailsResponse {
+        user: UserResponse {
+            id: user.id,
+            created_at: user.created_at.as_micros(),
+            status: user.status.as_code(),
+            username: WireName::new(user.username.as_ref())
+                .map_err(|_| IggyError::InvalidCommand)?,
+        },
+        permissions: user.permissions.as_deref().map(domain_permissions_to_wire),
+    };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/get_users_handler.rs b/core/server/src/binary/handlers/users/get_users_handler.rs
index df0d4bbff..aaccdeaa5 100644
--- a/core/server/src/binary/handlers/users/get_users_handler.rs
+++ b/core/server/src/binary/handlers/users/get_users_handler.rs
@@ -16,50 +16,39 @@
  * under the License.
  */
 
-use std::rc::Rc;
-
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
+use crate::binary::dispatch::HandlerResult;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
+use iggy_binary_protocol::WireName;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::responses::users::{GetUsersResponse, UserResponse};
 use iggy_common::IggyError;
 use iggy_common::SenderKind;
-use iggy_common::get_users::GetUsers;
+use std::rc::Rc;
 use tracing::debug;
 
-impl ServerCommandHandler for GetUsers {
-    fn code(&self) -> u32 {
-        iggy_common::GET_USERS_CODE
-    }
-
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-
-        let users = shard.metadata.query_users(session.get_user_id())?;
-        let response = mapper::map_users_meta(&users);
-        sender.send_ok_response(&response).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+pub async fn handle_get_users(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: get_users");
+    shard.ensure_authenticated(session)?;
 
-impl BinaryServerCommand for GetUsers {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::GetUsers(get_users) => Ok(get_users),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let users = shard.metadata.query_users(session.get_user_id())?;
+    let wire_users: Vec<UserResponse> = users
+        .iter()
+        .map(|u| {
+            Ok(UserResponse {
+                id: u.id,
+                created_at: u.created_at.as_micros(),
+                status: u.status.as_code(),
+                username: WireName::new(u.username.as_ref())
+                    .map_err(|_| IggyError::InvalidCommand)?,
+            })
+        })
+        .collect::<Result<_, IggyError>>()?;
+    let response = GetUsersResponse { users: wire_users };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/login_user_handler.rs b/core/server/src/binary/handlers/users/login_user_handler.rs
index 4836ffc7c..c9e42fdb3 100644
--- a/core/server/src/binary/handlers/users/login_user_handler.rs
+++ b/core/server/src/binary/handlers/users/login_user_handler.rs
@@ -16,68 +16,54 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::HandlerResult;
 use crate::binary::handlers::users::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::requests::users::LoginUserRequest;
+use iggy_binary_protocol::responses::users::IdentityResponse;
 use iggy_common::login_user::LoginUser;
-use iggy_common::{IggyError, SenderKind};
-use secrecy::ExposeSecret;
+use iggy_common::{IggyError, SenderKind, Validatable};
+use secrecy::SecretString;
 use std::rc::Rc;
 use tracing::{debug, info, instrument, warn};
 
-impl ServerCommandHandler for LoginUser {
-    fn code(&self) -> u32 {
-        iggy_common::LOGIN_USER_CODE
+#[instrument(skip_all, name = "trace_login_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_login_user(
+    req: LoginUserRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    if shard.is_shutting_down() {
+        warn!("Rejecting login request during shutdown");
+        return Err(IggyError::Disconnected);
     }
 
-    #[instrument(skip_all, name = "trace_login_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        if shard.is_shutting_down() {
-            warn!("Rejecting login request during shutdown");
-            return Err(IggyError::Disconnected);
-        }
+    let username = req.username.as_str();
+    debug!("session: {session}, command: login_user, username: {username}");
 
-        debug!("session: {session}, command: {self}");
-        let LoginUser {
-            username, password, ..
-        } = self;
-        info!("Logging in user: {} ...", &username);
-        let user = shard
-            .login_user(&username, password.expose_secret(), Some(session))
-            .error(|e: &IggyError| {
-                format!(
-                    "{COMPONENT} (error: {e}) - failed to login user with name: {}, session: {session}",
-                    username
-                )
-            })?;
-        info!("Logged in user: {} with ID: {}.", username, user.id);
+    let command = LoginUser {
+        username: username.to_string(),
+        password: SecretString::from(req.password.as_str()),
+        version: req.version.clone(),
+        context: req.context.clone(),
+    };
+    command.validate()?;
 
-        let identity_info = mapper::map_identity_info(user.id);
-        sender.send_ok_response(&identity_info).await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+    info!("Logging in user: {username} ...");
+    let user = shard
+        .login_user(username, &req.password, Some(session))
+        .error(|e: &IggyError| {
+            format!(
+                "{COMPONENT} (error: {e}) - failed to login user with name: {username}, session: {session}",
+            )
+        })?;
+    info!("Logged in user: {username} with ID: {}.", user.id);
 
-impl BinaryServerCommand for LoginUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::LoginUser(login_user) => Ok(login_user),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+    let response = IdentityResponse { user_id: user.id };
+    sender.send_ok_response(&response.to_bytes()).await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/logout_user_handler.rs b/core/server/src/binary/handlers/users/logout_user_handler.rs
index 39689cfa6..d3f5111f7 100644
--- a/core/server/src/binary/handlers/users/logout_user_handler.rs
+++ b/core/server/src/binary/handlers/users/logout_user_handler.rs
@@ -16,56 +16,29 @@
  * under the License.
  */
 
-use std::rc::Rc;
-
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
+use crate::binary::dispatch::HandlerResult;
 use crate::binary::handlers::users::COMPONENT;
-use crate::binary::handlers::utils::receive_and_validate;
-
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
 use err_trail::ErrContext;
-use iggy_common::logout_user::LogoutUser;
 use iggy_common::{IggyError, SenderKind};
-use tracing::info;
-use tracing::{debug, instrument};
-
-impl ServerCommandHandler for LogoutUser {
-    fn code(&self) -> u32 {
-        iggy_common::LOGOUT_USER_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_logout_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        info!("Logging out user with ID: {}...", session.get_user_id());
-        shard.logout_user(session).error(|e: &IggyError| {
-            format!("{COMPONENT} (error: {e}) - failed to logout user, session: {session}")
-        })?;
-        info!("Logged out user with ID: {}.", session.get_user_id());
-        session.clear_user_id();
-        sender.send_empty_ok_response().await?;
-        Ok(HandlerResult::Finished)
-    }
-}
+use std::rc::Rc;
+use tracing::{debug, info, instrument};
 
-impl BinaryServerCommand for LogoutUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::LogoutUser(logout_user) => Ok(logout_user),
-            _ => Err(IggyError::InvalidCommand),
-        }
-    }
+#[instrument(skip_all, name = "trace_logout_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_logout_user(
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!("session: {session}, command: logout_user");
+    shard.ensure_authenticated(session)?;
+    info!("Logging out user with ID: {}...", session.get_user_id());
+    shard.logout_user(session).error(|e: &IggyError| {
+        format!("{COMPONENT} (error: {e}) - failed to logout user, session: {session}")
+    })?;
+    info!("Logged out user with ID: {}.", session.get_user_id());
+    session.clear_user_id();
+    sender.send_empty_ok_response().await?;
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/update_permissions_handler.rs b/core/server/src/binary/handlers/users/update_permissions_handler.rs
index 0680b5b5d..b3243f3e7 100644
--- a/core/server/src/binary/handlers/users/update_permissions_handler.rs
+++ b/core/server/src/binary/handlers/users/update_permissions_handler.rs
@@ -16,68 +16,62 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
+use crate::binary::dispatch::{
+    HandlerResult, wire_id_to_identifier, wire_permissions_to_permissions,
 };
-use crate::binary::handlers::utils::receive_and_validate;
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::users::UpdatePermissionsRequest;
 use iggy_common::update_permissions::UpdatePermissions;
 use iggy_common::{IggyError, SenderKind};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for UpdatePermissions {
-    fn code(&self) -> u32 {
-        iggy_common::UPDATE_PERMISSIONS_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_update_permissions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard
-            .metadata
-            .perm_update_permissions(session.get_user_id())?;
+#[instrument(skip_all, name = "trace_update_permissions", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_update_permissions(
+    req: UpdatePermissionsRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: update_permissions, user_id: {:?}",
+        req.user_id
+    );
+    shard.ensure_authenticated(session)?;
+    shard
+        .metadata
+        .perm_update_permissions(session.get_user_id())?;
 
-        let target_user = shard.get_user(&self.user_id)?;
-        if target_user.is_root() {
-            return Err(IggyError::CannotChangePermissions(target_user.id));
-        }
+    let user_id = wire_id_to_identifier(&req.user_id)?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::UpdatePermissionsRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let target_user = shard.get_user(&user_id)?;
+    if target_user.is_root() {
+        return Err(IggyError::CannotChangePermissions(target_user.id));
+    }
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::UpdatePermissionsResponse => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected UpdatePermissionsResponse"),
-        }
+    let command = UpdatePermissions {
+        user_id,
+        permissions: req
+            .permissions
+            .as_ref()
+            .map(wire_permissions_to_permissions),
+    };
 
-        Ok(HandlerResult::Finished)
-    }
-}
+    let request = ShardRequest::control_plane(ShardRequestPayload::UpdatePermissionsRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-impl BinaryServerCommand for UpdatePermissions {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::UpdatePermissions(update_permissions) => Ok(update_permissions),
-            _ => Err(IggyError::InvalidCommand),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::UpdatePermissionsResponse => {
+            sender.send_empty_ok_response().await?;
         }
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected UpdatePermissionsResponse"),
     }
+
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/users/update_user_handler.rs b/core/server/src/binary/handlers/users/update_user_handler.rs
index d6ea576f4..397490319 100644
--- a/core/server/src/binary/handlers/users/update_user_handler.rs
+++ b/core/server/src/binary/handlers/users/update_user_handler.rs
@@ -16,61 +16,53 @@
  * under the License.
  */
 
-use crate::binary::command::{
-    BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
-};
-use crate::binary::handlers::utils::receive_and_validate;
+use crate::binary::dispatch::{HandlerResult, wire_id_to_identifier};
 use crate::shard::IggyShard;
 use crate::shard::transmission::frame::ShardResponse;
 use crate::shard::transmission::message::{ShardRequest, ShardRequestPayload};
 use crate::streaming::session::Session;
+use iggy_binary_protocol::requests::users::UpdateUserRequest;
 use iggy_common::update_user::UpdateUser;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{IggyError, SenderKind, UserStatus, Validatable};
 use std::rc::Rc;
 use tracing::{debug, instrument};
 
-impl ServerCommandHandler for UpdateUser {
-    fn code(&self) -> u32 {
-        iggy_common::UPDATE_USER_CODE
-    }
-
-    #[instrument(skip_all, name = "trace_update_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
-    async fn handle(
-        self,
-        sender: &mut SenderKind,
-        _length: u32,
-        session: &Session,
-        shard: &Rc<IggyShard>,
-    ) -> Result<HandlerResult, IggyError> {
-        debug!("session: {session}, command: {self}");
-        shard.ensure_authenticated(session)?;
-        shard.metadata.perm_update_user(session.get_user_id())?;
+#[instrument(skip_all, name = "trace_update_user", fields(iggy_user_id = session.get_user_id(), iggy_client_id = session.client_id))]
+pub async fn handle_update_user(
+    req: UpdateUserRequest,
+    sender: &mut SenderKind,
+    session: &Session,
+    shard: &Rc<IggyShard>,
+) -> Result<HandlerResult, IggyError> {
+    debug!(
+        "session: {session}, command: update_user, user_id: {:?}",
+        req.user_id
+    );
+    shard.ensure_authenticated(session)?;
+    shard.metadata.perm_update_user(session.get_user_id())?;
 
-        let request = ShardRequest::control_plane(ShardRequestPayload::UpdateUserRequest {
-            user_id: session.get_user_id(),
-            command: self,
-        });
+    let user_id = wire_id_to_identifier(&req.user_id)?;
+    let status = req.status.map(UserStatus::from_code).transpose()?;
 
-        match shard.send_to_control_plane(request).await? {
-            ShardResponse::UpdateUserResponse(_) => {
-                sender.send_empty_ok_response().await?;
-            }
-            ShardResponse::ErrorResponse(err) => return Err(err),
-            _ => unreachable!("Expected UpdateUserResponse"),
-        }
+    let command = UpdateUser {
+        user_id,
+        username: req.username.map(|n| n.to_string()),
+        status,
+    };
+    command.validate()?;
 
-        Ok(HandlerResult::Finished)
-    }
-}
+    let request = ShardRequest::control_plane(ShardRequestPayload::UpdateUserRequest {
+        user_id: session.get_user_id(),
+        command,
+    });
 
-impl BinaryServerCommand for UpdateUser {
-    async fn from_sender(sender: &mut SenderKind, code: u32, length: u32) -> Result<Self, IggyError>
-    where
-        Self: Sized,
-    {
-        match receive_and_validate(sender, code, length).await? {
-            ServerCommand::UpdateUser(update_user) => Ok(update_user),
-            _ => Err(IggyError::InvalidCommand),
+    match shard.send_to_control_plane(request).await? {
+        ShardResponse::UpdateUserResponse(_) => {
+            sender.send_empty_ok_response().await?;
         }
+        ShardResponse::ErrorResponse(err) => return Err(err),
+        _ => unreachable!("Expected UpdateUserResponse"),
     }
+
+    Ok(HandlerResult::Finished)
 }
diff --git a/core/server/src/binary/handlers/utils.rs b/core/server/src/binary/handlers/utils.rs
deleted file mode 100644
index d07939333..000000000
--- a/core/server/src/binary/handlers/utils.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::binary::command::ServerCommand;
-use bytes::BytesMut;
-use iggy_common::{IggyError, SenderKind};
-
-pub async fn receive_and_validate(
-    sender: &mut SenderKind,
-    code: u32,
-    length: u32,
-) -> Result<ServerCommand, IggyError> {
-    let mut buffer = BytesMut::with_capacity(length as usize);
-    unsafe {
-        buffer.set_len(length as usize);
-    }
-    let buffer = if length == 0 {
-        buffer
-    } else {
-        let (result, buffer) = sender.read(buffer).await;
-        result?;
-        buffer
-    };
-
-    let command = ServerCommand::from_code_and_payload(code, buffer.freeze())?;
-    command.validate()?;
-    Ok(command)
-}
diff --git a/core/server/src/binary/macros.rs b/core/server/src/binary/macros.rs
deleted file mode 100644
index 655705088..000000000
--- a/core/server/src/binary/macros.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/// This macro does 4 expansions in one go:
-///
-/// 1) The `#[enum_dispatch(ServerCommandHandler)] pub enum ServerCommand`
-///    with all variants.
-/// 2) The `from_code_and_payload(code, payload)` function that matches each code.
-/// 3) The `to_bytes()` function that matches each variant.
-/// 4) The `validate()` function that matches each variant.
-#[macro_export]
-macro_rules! define_server_command_enum {
-    (
-                $(
-            // Macro pattern:
-            // variant name     inner type    numeric code   display string   show_payload?
-            $variant:ident ( $ty:ty ), $code:ident, $display_str:expr, $show_payload:expr
-        );* $(;)?
-    ) => {
-        #[enum_dispatch(ServerCommandHandler)]
-        #[derive(Debug, EnumString, EnumIter)]
-        pub enum ServerCommand {
-            $(
-                $variant($ty),
-            )*
-        }
-
-        impl ServerCommand {
-            /// Constructs a `ServerCommand` from its numeric code and payload.
-            pub fn from_code_and_payload(code: u32, payload: Bytes) -> Result<Self, IggyError> {
-                match code {
-                    $(
-                        $code => Ok(ServerCommand::$variant(
-                            <$ty>::from_bytes(payload)?
-                        )),
-                    )*
-                    _ => {
-                        error!("Invalid server command: {}", code);
-                        Err(IggyError::InvalidCommand)
-                    }
-                }
-            }
-
-            /// Constructs a ServerCommand from its numeric code by reading from the provided async reader.
-            pub async fn from_code_and_reader(
-                code: u32,
-                sender: &mut SenderKind,
-                length: u32,
-            ) -> Result<Self, IggyError> {
-                match code {
-                    $(
-                        $code => Ok(ServerCommand::$variant(
-                            <$ty as BinaryServerCommand>::from_sender(sender, code, length).await?
-                        )),
-                    )*
-                    _ => Err(IggyError::InvalidCommand),
-                }
-            }
-
-            /// Converts the command into raw bytes.
-            pub fn to_bytes(&self) -> Bytes {
-                match self {
-                    $(
-                        ServerCommand::$variant(payload) => as_bytes(payload),
-                    )*
-                }
-            }
-
-            /// Validate the command by delegating to the inner command's implementation.
-            pub fn validate(&self) -> Result<(), IggyError> {
-                match self {
-                    $(
-                        ServerCommand::$variant(cmd) => <$ty as iggy_common::Validatable<iggy_common::IggyError>>::validate(cmd),
-                    )*
-                }
-            }
-        }
-
-
-        impl std::fmt::Display for ServerCommand {
-            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-                match self {
-                    $(
-                        // If $show_payload is true, we display the command name + payload;
-                        // otherwise, we just display the command name.
-                        ServerCommand::$variant(payload) => {
-                            if $show_payload {
-                                write!(formatter, "{}|{payload:?}", $display_str)
-                            } else {
-                                write!(formatter, "{}", $display_str)
-                            }
-                        },
-                    )*
-                }
-            }
-        }
-    };
-
-}
diff --git a/core/server/src/binary/mapper.rs b/core/server/src/binary/mapper.rs
deleted file mode 100644
index d439700d5..000000000
--- a/core/server/src/binary/mapper.rs
+++ /dev/null
@@ -1,431 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::metadata::{ConsumerGroupMeta, StreamMeta, TopicMeta, UserMeta};
-use crate::shard::transmission::frame::{
-    ConsumerGroupResponseData, StreamResponseData, TopicResponseData,
-};
-use crate::streaming::clients::client_manager::Client;
-use crate::streaming::users::user::User;
-use bytes::{BufMut, Bytes, BytesMut};
-use iggy_common::{
-    BytesSerializable, ConsumerOffsetInfo, PersonalAccessToken, Stats, TransportProtocol, UserId,
-};
-
-pub fn map_stats(stats: &Stats) -> Bytes {
-    let mut bytes = BytesMut::with_capacity(104);
-    bytes.put_u32_le(stats.process_id);
-    bytes.put_f32_le(stats.cpu_usage);
-    bytes.put_f32_le(stats.total_cpu_usage);
-    bytes.put_u64_le(stats.memory_usage.as_bytes_u64());
-    bytes.put_u64_le(stats.total_memory.as_bytes_u64());
-    bytes.put_u64_le(stats.available_memory.as_bytes_u64());
-    bytes.put_u64_le(stats.run_time.into());
-    bytes.put_u64_le(stats.start_time.into());
-    bytes.put_u64_le(stats.read_bytes.as_bytes_u64());
-    bytes.put_u64_le(stats.written_bytes.as_bytes_u64());
-    bytes.put_u64_le(stats.messages_size_bytes.as_bytes_u64());
-    bytes.put_u32_le(stats.streams_count);
-    bytes.put_u32_le(stats.topics_count);
-    bytes.put_u32_le(stats.partitions_count);
-    bytes.put_u32_le(stats.segments_count);
-    bytes.put_u64_le(stats.messages_count);
-    bytes.put_u32_le(stats.clients_count);
-    bytes.put_u32_le(stats.consumer_groups_count);
-    bytes.put_u32_le(stats.hostname.len() as u32);
-    bytes.put_slice(stats.hostname.as_bytes());
-    bytes.put_u32_le(stats.os_name.len() as u32);
-    bytes.put_slice(stats.os_name.as_bytes());
-    bytes.put_u32_le(stats.os_version.len() as u32);
-    bytes.put_slice(stats.os_version.as_bytes());
-    bytes.put_u32_le(stats.kernel_version.len() as u32);
-    bytes.put_slice(stats.kernel_version.as_bytes());
-    bytes.put_u32_le(stats.iggy_server_version.len() as u32);
-    bytes.put_slice(stats.iggy_server_version.as_bytes());
-    if let Some(semver) = stats.iggy_server_semver {
-        bytes.put_u32_le(semver);
-    }
-
-    bytes.put_u32_le(stats.cache_metrics.len() as u32);
-    for (key, metrics) in &stats.cache_metrics {
-        bytes.put_u32_le(key.stream_id);
-        bytes.put_u32_le(key.topic_id);
-        bytes.put_u32_le(key.partition_id);
-
-        bytes.put_u64_le(metrics.hits);
-        bytes.put_u64_le(metrics.misses);
-        bytes.put_f32_le(metrics.hit_ratio);
-    }
-
-    bytes.put_u32_le(stats.threads_count);
-    bytes.put_u64_le(stats.free_disk_space.as_bytes_u64());
-    bytes.put_u64_le(stats.total_disk_space.as_bytes_u64());
-
-    bytes.freeze()
-}
-
-pub fn map_consumer_offset(offset: &ConsumerOffsetInfo) -> Bytes {
-    let mut bytes = BytesMut::with_capacity(20);
-    bytes.put_u32_le(offset.partition_id);
-    bytes.put_u64_le(offset.current_offset);
-    bytes.put_u64_le(offset.stored_offset);
-    bytes.freeze()
-}
-
-pub fn map_client(client: &Client) -> Bytes {
-    let mut bytes = BytesMut::new();
-    extend_client(client, &mut bytes);
-    for consumer_group in &client.consumer_groups {
-        bytes.put_u32_le(consumer_group.stream_id);
-        bytes.put_u32_le(consumer_group.topic_id);
-        bytes.put_u32_le(consumer_group.group_id);
-    }
-    bytes.freeze()
-}
-
-pub async fn map_clients(clients: Vec<Client>) -> Bytes {
-    let mut bytes = BytesMut::new();
-    for client in clients.iter() {
-        extend_client(client, &mut bytes);
-    }
-    bytes.freeze()
-}
-
-pub fn map_user(user: &User) -> Bytes {
-    let mut bytes = BytesMut::new();
-    extend_user(user, &mut bytes);
-    if let Some(permissions) = &user.permissions {
-        bytes.put_u8(1);
-        let permissions = permissions.to_bytes();
-        #[allow(clippy::cast_possible_truncation)]
-        bytes.put_u32_le(permissions.len() as u32);
-        bytes.put_slice(&permissions);
-    } else {
-        bytes.put_u32_le(0);
-    }
-    bytes.freeze()
-}
-
-pub fn map_users(users: Vec<User>) -> Bytes {
-    let mut bytes = BytesMut::new();
-    for user in users.iter() {
-        extend_user(user, &mut bytes);
-    }
-    bytes.freeze()
-}
-
-pub fn map_identity_info(user_id: UserId) -> Bytes {
-    let mut bytes = BytesMut::with_capacity(4);
-    bytes.put_u32_le(user_id);
-    bytes.freeze()
-}
-
-pub fn map_raw_pat(token: &str) -> Bytes {
-    let mut bytes = BytesMut::with_capacity(1 + token.len());
-    bytes.put_u8(token.len() as u8);
-    bytes.put_slice(token.as_bytes());
-    bytes.freeze()
-}
-
-pub fn map_personal_access_tokens(personal_access_tokens: Vec<PersonalAccessToken>) -> Bytes {
-    let mut bytes = BytesMut::new();
-    for personal_access_token in personal_access_tokens.iter() {
-        extend_pat(personal_access_token, &mut bytes);
-    }
-    bytes.freeze()
-}
-
-pub fn map_stream_from_response(data: &StreamResponseData) -> Bytes {
-    let mut bytes = BytesMut::new();
-    bytes.put_u32_le(data.id);
-    bytes.put_u64_le(data.created_at.into());
-    bytes.put_u32_le(0); // topics_count = 0 for new stream
-    bytes.put_u64_le(0); // total_size = 0
-    bytes.put_u64_le(0); // total_messages = 0
-    bytes.put_u8(data.name.len() as u8);
-    bytes.put_slice(data.name.as_bytes());
-    bytes.freeze()
-}
-
-pub fn map_topic_from_response(data: &TopicResponseData) -> Bytes {
-    let mut bytes = BytesMut::new();
-    bytes.put_u32_le(data.id);
-    bytes.put_u64_le(data.created_at.into());
-    bytes.put_u32_le(data.partitions.len() as u32);
-    bytes.put_u64_le(data.message_expiry.into());
-    bytes.put_u8(data.compression_algorithm.as_code());
-    bytes.put_u64_le(data.max_topic_size.into());
-    bytes.put_u8(data.replication_factor);
-    bytes.put_u64_le(0); // topic_size = 0
-    bytes.put_u64_le(0); // topic_messages = 0
-    bytes.put_u8(data.name.len() as u8);
-    bytes.put_slice(data.name.as_bytes());
-
-    for partition in &data.partitions {
-        bytes.put_u32_le(partition.id as u32);
-        bytes.put_u64_le(partition.created_at.into());
-        bytes.put_u32_le(0); // segments_count = 0 for new partition
-        bytes.put_u64_le(0); // current_offset = 0
-        bytes.put_u64_le(0); // size_bytes = 0
-        bytes.put_u64_le(0); // messages_count = 0
-    }
-
-    bytes.freeze()
-}
-
-pub fn map_consumer_group_from_response(data: &ConsumerGroupResponseData) -> Bytes {
-    let mut bytes = BytesMut::new();
-    bytes.put_u32_le(data.id);
-    bytes.put_u32_le(data.partitions_count);
-    bytes.put_u32_le(0); // members_count = 0 for new group
-    bytes.put_u8(data.name.len() as u8);
-    bytes.put_slice(data.name.as_bytes());
-    bytes.freeze()
-}
-
-/// Map consumer group from SharedMetadata format.
-pub fn map_consumer_group_from_meta(meta: &ConsumerGroupMeta) -> Bytes {
-    let mut bytes = BytesMut::new();
-
-    // Header: id, partitions_count, members_count, name_len, name
-    bytes.put_u32_le(meta.id as u32);
-    bytes.put_u32_le(meta.partitions.len() as u32);
-    bytes.put_u32_le(meta.members.len() as u32);
-    bytes.put_u8(meta.name.len() as u8);
-    bytes.put_slice(meta.name.as_bytes());
-
-    // Members
-    for (_, member) in meta.members.iter() {
-        bytes.put_u32_le(member.id as u32);
-        bytes.put_u32_le(member.partitions.len() as u32);
-        for &partition_id in &member.partitions {
-            bytes.put_u32_le(partition_id as u32);
-        }
-    }
-
-    bytes.freeze()
-}
-
-fn extend_client(client: &Client, bytes: &mut BytesMut) {
-    bytes.put_u32_le(client.session.client_id);
-    bytes.put_u32_le(client.user_id.unwrap_or(u32::MAX));
-    let transport: u8 = match client.transport {
-        TransportProtocol::Tcp => 1,
-        TransportProtocol::Quic => 2,
-        TransportProtocol::Http => 3,
-        TransportProtocol::WebSocket => 4,
-    };
-    bytes.put_u8(transport);
-    let address = client.session.ip_address.to_string();
-    bytes.put_u32_le(address.len() as u32);
-    bytes.put_slice(address.as_bytes());
-    bytes.put_u32_le(client.consumer_groups.len() as u32);
-}
-
-fn extend_user(user: &User, bytes: &mut BytesMut) {
-    bytes.put_u32_le(user.id);
-    bytes.put_u64_le(user.created_at.into());
-    bytes.put_u8(user.status.as_code());
-    bytes.put_u8(user.username.len() as u8);
-    bytes.put_slice(user.username.as_bytes());
-}
-
-fn extend_pat(personal_access_token: &PersonalAccessToken, bytes: &mut BytesMut) {
-    bytes.put_u8(personal_access_token.name.len() as u8);
-    bytes.put_slice(personal_access_token.name.as_bytes());
-    match &personal_access_token.expiry_at {
-        Some(expiry_at) => {
-            bytes.put_u64_le(expiry_at.as_micros());
-        }
-        None => {
-            bytes.put_u64_le(0);
-        }
-    }
-}
-
-fn compute_stream_stats(stream: &StreamMeta) -> (u64, u64) {
-    let mut size = 0u64;
-    let mut messages = 0u64;
-    for (_, topic) in stream.topics.iter() {
-        for partition in topic.partitions.iter() {
-            size += partition.stats.size_bytes_inconsistent();
-            messages += partition.stats.messages_count_inconsistent();
-        }
-    }
-    (size, messages)
-}
-
-fn compute_topic_stats(topic: &TopicMeta) -> (u64, u64) {
-    let mut size = 0u64;
-    let mut messages = 0u64;
-    for partition in topic.partitions.iter() {
-        size += partition.stats.size_bytes_inconsistent();
-        messages += partition.stats.messages_count_inconsistent();
-    }
-    (size, messages)
-}
-
-fn extend_topic_header(topic: &TopicMeta, bytes: &mut BytesMut) {
-    let (size, messages) = compute_topic_stats(topic);
-    bytes.put_u32_le(topic.id as u32);
-    bytes.put_u64_le(topic.created_at.into());
-    bytes.put_u32_le(topic.partitions.len() as u32);
-    bytes.put_u64_le(topic.message_expiry.into());
-    bytes.put_u8(topic.compression_algorithm.as_code());
-    bytes.put_u64_le(topic.max_topic_size.into());
-    bytes.put_u8(topic.replication_factor);
-    bytes.put_u64_le(size);
-    bytes.put_u64_le(messages);
-    bytes.put_u8(topic.name.len() as u8);
-    bytes.put_slice(topic.name.as_bytes());
-}
-
-fn extend_topic_with_partitions(topic: &TopicMeta, bytes: &mut BytesMut) {
-    extend_topic_header(topic, bytes);
-
-    for (partition_id, partition) in topic.partitions.iter().enumerate() {
-        let created_at = partition.created_at;
-        let segments_count = partition.stats.segments_count_inconsistent();
-        let offset = partition.stats.current_offset();
-        let size_bytes = partition.stats.size_bytes_inconsistent();
-        let messages_count = partition.stats.messages_count_inconsistent();
-
-        bytes.put_u32_le(partition_id as u32);
-        bytes.put_u64_le(created_at.into());
-        bytes.put_u32_le(segments_count);
-        bytes.put_u64_le(offset);
-        bytes.put_u64_le(size_bytes);
-        bytes.put_u64_le(messages_count);
-    }
-}
-
-/// Map a single stream metadata to wire format (includes nested topics).
-pub fn map_stream(stream: &StreamMeta) -> Bytes {
-    let mut bytes = BytesMut::new();
-
-    let mut topic_ids: Vec<_> = stream.topics.iter().map(|(k, _)| k).collect();
-    topic_ids.sort_unstable();
-
-    let (total_size, total_messages) = compute_stream_stats(stream);
-
-    bytes.put_u32_le(stream.id as u32);
-    bytes.put_u64_le(stream.created_at.into());
-    bytes.put_u32_le(topic_ids.len() as u32);
-    bytes.put_u64_le(total_size);
-    bytes.put_u64_le(total_messages);
-    bytes.put_u8(stream.name.len() as u8);
-    bytes.put_slice(stream.name.as_bytes());
-
-    for &topic_id in &topic_ids {
-        if let Some(topic) = stream.topics.get(topic_id) {
-            extend_topic_header(topic, &mut bytes);
-        }
-    }
-
-    bytes.freeze()
-}
-
-/// Map multiple streams to wire format (header only, no nested topics).
-pub fn map_streams(streams: &[StreamMeta]) -> Bytes {
-    let mut bytes = BytesMut::new();
-
-    let mut sorted: Vec<_> = streams.iter().collect();
-    sorted.sort_by_key(|s| s.id);
-
-    for stream in sorted {
-        let (total_size, total_messages) = compute_stream_stats(stream);
-        let topics_count = stream.topics.len();
-
-        bytes.put_u32_le(stream.id as u32);
-        bytes.put_u64_le(stream.created_at.into());
-        bytes.put_u32_le(topics_count as u32);
-        bytes.put_u64_le(total_size);
-        bytes.put_u64_le(total_messages);
-        bytes.put_u8(stream.name.len() as u8);
-        bytes.put_slice(stream.name.as_bytes());
-    }
-
-    bytes.freeze()
-}
-
-/// Map a single topic metadata to wire format (includes partitions).
-pub fn map_topic(topic: &TopicMeta) -> Bytes {
-    let mut bytes = BytesMut::new();
-    extend_topic_with_partitions(topic, &mut bytes);
-    bytes.freeze()
-}
-
-/// Map multiple topics to wire format (header only, no partitions).
-pub fn map_topics(topics: &[TopicMeta]) -> Bytes {
-    let mut bytes = BytesMut::new();
-
-    let mut sorted: Vec<_> = topics.iter().collect();
-    sorted.sort_by_key(|t| t.id);
-
-    for topic in sorted {
-        extend_topic_header(topic, &mut bytes);
-    }
-
-    bytes.freeze()
-}
-
-/// Map multiple consumer groups to wire format.
-pub fn map_consumer_groups(groups: &[ConsumerGroupMeta]) -> Bytes {
-    let mut bytes = BytesMut::new();
-    for cg in groups {
-        bytes.put_u32_le(cg.id as u32);
-        bytes.put_u32_le(cg.partitions.len() as u32);
-        bytes.put_u32_le(cg.members.len() as u32);
-        bytes.put_u8(cg.name.len() as u8);
-        bytes.put_slice(cg.name.as_bytes());
-    }
-    bytes.freeze()
-}
-
-fn extend_user_meta(user: &UserMeta, bytes: &mut BytesMut) {
-    bytes.put_u32_le(user.id);
-    bytes.put_u64_le(user.created_at.into());
-    bytes.put_u8(user.status.as_code());
-    bytes.put_u8(user.username.len() as u8);
-    bytes.put_slice(user.username.as_bytes());
-}
-
-/// Map a single user metadata to wire format.
-pub fn map_user_meta(user: &UserMeta) -> Bytes {
-    let mut bytes = BytesMut::new();
-    extend_user_meta(user, &mut bytes);
-    if let Some(permissions) = &user.permissions {
-        bytes.put_u8(1);
-        let permissions = permissions.to_bytes();
-        #[allow(clippy::cast_possible_truncation)]
-        bytes.put_u32_le(permissions.len() as u32);
-        bytes.put_slice(&permissions);
-    } else {
-        bytes.put_u32_le(0);
-    }
-    bytes.freeze()
-}
-
-/// Map multiple user metadata to wire format.
-pub fn map_users_meta(users: &[UserMeta]) -> Bytes {
-    let mut bytes = BytesMut::new();
-    for user in users {
-        extend_user_meta(user, &mut bytes);
-    }
-    bytes.freeze()
-}
diff --git a/core/server/src/binary/mod.rs b/core/server/src/binary/mod.rs
index ebca4eb7b..f46213b29 100644
--- a/core/server/src/binary/mod.rs
+++ b/core/server/src/binary/mod.rs
@@ -16,11 +16,7 @@
  * under the License.
  */
 
-pub mod command;
+pub mod dispatch;
 pub mod handlers;
-pub mod mapper;
-
-#[macro_use]
-mod macros;
 
 pub const COMPONENT: &str = "BINARY";
diff --git a/core/server/src/http/partitions.rs b/core/server/src/http/partitions.rs
index 96570a711..fd2930731 100644
--- a/core/server/src/http/partitions.rs
+++ b/core/server/src/http/partitions.rs
@@ -59,7 +59,7 @@ async fn create_partitions(
     });
 
     match state.shard.send_to_control_plane(request).await? {
-        ShardResponse::CreatePartitionsResponse(_) => Ok(StatusCode::CREATED),
+        ShardResponse::CreatePartitionsResponse => Ok(StatusCode::CREATED),
         ShardResponse::ErrorResponse(err) => Err(err.into()),
         _ => unreachable!("Expected CreatePartitionsResponse"),
     }
@@ -87,7 +87,7 @@ async fn delete_partitions(
     });
 
     match state.shard.send_to_control_plane(request).await? {
-        ShardResponse::DeletePartitionsResponse(_) => Ok(StatusCode::NO_CONTENT),
+        ShardResponse::DeletePartitionsResponse => Ok(StatusCode::NO_CONTENT),
         ShardResponse::ErrorResponse(err) => Err(err.into()),
         _ => unreachable!("Expected DeletePartitionsResponse"),
     }
diff --git a/core/server/src/http/streams.rs b/core/server/src/http/streams.rs
index 92a623c66..80f216283 100644
--- a/core/server/src/http/streams.rs
+++ b/core/server/src/http/streams.rs
@@ -158,7 +158,7 @@ async fn delete_stream(
     });
 
     match state.shard.send_to_control_plane(request).await? {
-        ShardResponse::DeleteStreamResponse(_) => Ok(StatusCode::NO_CONTENT),
+        ShardResponse::DeleteStreamResponse => Ok(StatusCode::NO_CONTENT),
         ShardResponse::ErrorResponse(err) => Err(err.into()),
         _ => unreachable!("Expected DeleteStreamResponse"),
     }
diff --git a/core/server/src/http/topics.rs b/core/server/src/http/topics.rs
index 7e170313b..857385bee 100644
--- a/core/server/src/http/topics.rs
+++ b/core/server/src/http/topics.rs
@@ -210,7 +210,7 @@ async fn delete_topic(
     });
 
     match state.shard.send_to_control_plane(request).await? {
-        ShardResponse::DeleteTopicResponse(_) => Ok(StatusCode::NO_CONTENT),
+        ShardResponse::DeleteTopicResponse => Ok(StatusCode::NO_CONTENT),
         ShardResponse::ErrorResponse(err) => Err(err.into()),
         _ => unreachable!("Expected DeleteTopicResponse"),
     }
diff --git a/core/server/src/quic/listener.rs b/core/server/src/quic/listener.rs
index b9d14a9a8..a39abae56 100644
--- a/core/server/src/quic/listener.rs
+++ b/core/server/src/quic/listener.rs
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-use crate::binary::command::{ServerCommand, ServerCommandHandler};
+use crate::binary::dispatch::{self, HandlerResult, MAX_CONTROL_FRAME_PAYLOAD};
 use crate::server_error::ConnectionError;
 use crate::shard::IggyShard;
 use crate::shard::task_registry::ShutdownToken;
@@ -25,11 +25,11 @@ use anyhow::anyhow;
 use compio::io::AsyncReadExt;
 use compio::quic::{Connection, Endpoint, RecvStream, SendStream};
 use futures::FutureExt;
+use iggy_binary_protocol::RequestFrame;
+use iggy_binary_protocol::codes::{SEND_MESSAGES_CODE, command_name};
 use iggy_common::{GET_CLUSTER_METADATA_CODE, IggyError, SenderKind, TransportProtocol};
 use std::rc::Rc;
-use tracing::{debug, error, info, trace};
-
-const INITIAL_BYTES_LENGTH: usize = 4;
+use tracing::{debug, error, info, trace, warn};
 
 pub async fn start(
     endpoint: Endpoint,
@@ -165,39 +165,47 @@ async fn handle_stream(
 ) -> anyhow::Result<()> {
     let (send_stream, mut recv_stream) = stream;
 
-    let length_buffer = [0u8; INITIAL_BYTES_LENGTH];
-    let code_buffer = [0u8; INITIAL_BYTES_LENGTH];
-
-    let compio::BufResult(result, length_buffer) = recv_stream.read_exact(length_buffer).await;
-    result?;
-    let compio::BufResult(result, code_buffer) = recv_stream.read_exact(code_buffer).await;
+    let header_buf = [0u8; RequestFrame::HEADER_SIZE];
+    let compio::BufResult(result, header_buf) = recv_stream.read_exact(header_buf).await;
     result?;
 
-    let length = u32::from_le_bytes(length_buffer);
-    let code = u32::from_le_bytes(code_buffer);
+    let length = u32::from_le_bytes(header_buf[0..4].try_into().unwrap());
+    let code = u32::from_le_bytes(header_buf[4..8].try_into().unwrap());
+
+    let cmd_name = command_name(code).unwrap_or("unknown");
+    trace!("Received a QUIC request, length: {length}, code: {code} ({cmd_name})");
 
-    trace!("Received a QUIC request, length: {length}, code: {code}");
+    let payload_length = RequestFrame::payload_length(length)
+        .map_err(|_| anyhow!("Invalid frame length: {length}"))?;
 
     let mut sender = SenderKind::get_quic_sender(send_stream, recv_stream);
 
-    let command = match ServerCommand::from_code_and_reader(code, &mut sender, length - 4).await {
-        Ok(cmd) => cmd,
-        Err(e) => {
-            sender.send_error_response(e.clone()).await?;
-            return Err(anyhow!("Failed to parse command: {e}"));
+    let result = if code == SEND_MESSAGES_CODE {
+        dispatch::dispatch_send_messages(&mut sender, payload_length, session, &shard).await
+    } else {
+        if payload_length > MAX_CONTROL_FRAME_PAYLOAD {
+            sender
+                .send_error_response(IggyError::InvalidCommand)
+                .await?;
+            return Ok(());
         }
+        let payload = dispatch::read_payload(&mut sender, payload_length).await?;
+        let frame = RequestFrame::from_parts(code, &payload);
+        dispatch::dispatch(frame, &mut sender, session, &shard).await
     };
 
-    trace!("Received a QUIC command: {command}, payload size: {length}");
-
-    match command.handle(&mut sender, length, session, &shard).await {
-        Ok(_) => {
+    match result {
+        Ok(HandlerResult::Finished) => {
             trace!(
                 "Command was handled successfully, session: {:?}. QUIC response was sent.",
                 session
             );
             Ok(())
         }
+        Ok(HandlerResult::Migrated { to_shard }) => {
+            warn!("Unexpected migration on QUIC: to_shard {to_shard}, session: {session:?}");
+            Ok(())
+        }
         Err(e) => {
             // Special handling for GetClusterMetadata when clustering is disabled
             if code == GET_CLUSTER_METADATA_CODE && matches!(e, IggyError::FeatureUnavailable) {
diff --git a/core/server/src/shard/execution.rs b/core/server/src/shard/execution.rs
index 1cdaea83d..5e425d7af 100644
--- a/core/server/src/shard/execution.rs
+++ b/core/server/src/shard/execution.rs
@@ -47,21 +47,6 @@ use iggy_common::{
     update_stream::UpdateStream, update_topic::UpdateTopic, update_user::UpdateUser,
 };
 use secrecy::{ExposeSecret, SecretString};
-pub struct DeleteStreamResult {
-    pub stream_id: usize,
-}
-
-pub struct DeleteTopicResult {
-    pub topic_id: usize,
-}
-
-pub struct CreatePartitionsResult {
-    pub partition_ids: Vec<usize>,
-}
-
-pub struct DeletePartitionsResult {
-    pub partition_ids: Vec<usize>,
-}
 
 pub async fn execute_create_stream(
     shard: &IggyShard,
@@ -121,7 +106,7 @@ pub async fn execute_delete_stream(
     shard: &IggyShard,
     user_id: u32,
     command: DeleteStream,
-) -> Result<DeleteStreamResult, IggyError> {
+) -> Result<(), IggyError> {
     let stream = shard.resolve_stream(&command.stream_id)?;
     shard.metadata.perm_delete_stream(user_id, stream.id())?;
 
@@ -136,7 +121,7 @@ pub async fn execute_delete_stream(
         })
         .collect();
 
-    let stream_info = shard.delete_stream(stream).await?;
+    shard.delete_stream(stream).await?;
 
     shard
         .state
@@ -161,9 +146,7 @@ pub async fn execute_delete_stream(
         }
     }
 
-    Ok(DeleteStreamResult {
-        stream_id: stream_info.id,
-    })
+    Ok(())
 }
 
 pub async fn execute_purge_stream(
@@ -293,7 +276,7 @@ pub async fn execute_delete_topic(
     shard: &IggyShard,
     user_id: u32,
     command: DeleteTopic,
-) -> Result<DeleteTopicResult, IggyError> {
+) -> Result<(), IggyError> {
     let topic = shard.resolve_topic(&command.stream_id, &command.topic_id)?;
     shard
         .metadata
@@ -304,7 +287,7 @@ pub async fn execute_delete_topic(
         .metadata
         .get_partition_ids(topic.stream_id, topic.topic_id);
 
-    let topic_info = shard.delete_topic(topic).await?;
+    shard.delete_topic(topic).await?;
 
     shard
         .state
@@ -324,9 +307,7 @@ pub async fn execute_delete_topic(
         tracing::warn!("Broadcast failed: {e}. Shards will sync on restart.");
     }
 
-    Ok(DeleteTopicResult {
-        topic_id: topic_info.id,
-    })
+    Ok(())
 }
 
 pub async fn execute_purge_topic(
@@ -364,7 +345,7 @@ pub async fn execute_create_partitions(
     shard: &IggyShard,
     user_id: u32,
     command: CreatePartitions,
-) -> Result<CreatePartitionsResult, IggyError> {
+) -> Result<(), IggyError> {
     let topic = shard.resolve_topic(&command.stream_id, &command.topic_id)?;
     shard
         .metadata
@@ -373,8 +354,6 @@ pub async fn execute_create_partitions(
     let partition_infos = shard
         .create_partitions(topic, command.partitions_count)
         .await?;
-    let partition_ids = partition_infos.iter().map(|p| p.id).collect::<Vec<_>>();
-
     let total_partition_count = shard
         .metadata
         .partitions_count(topic.stream_id, topic.topic_id) as u32;
@@ -400,14 +379,14 @@ pub async fn execute_create_partitions(
         tracing::warn!("Broadcast failed: {e}. Shards will sync on restart.");
     }
 
-    Ok(CreatePartitionsResult { partition_ids })
+    Ok(())
 }
 
 pub async fn execute_delete_partitions(
     shard: &IggyShard,
     user_id: u32,
     command: DeletePartitions,
-) -> Result<DeletePartitionsResult, IggyError> {
+) -> Result<(), IggyError> {
     let topic = shard.resolve_topic(&command.stream_id, &command.topic_id)?;
     shard
         .metadata
@@ -444,9 +423,7 @@ pub async fn execute_delete_partitions(
         tracing::warn!("Broadcast failed: {e}. Shards will sync on restart.");
     }
 
-    Ok(DeletePartitionsResult {
-        partition_ids: deleted_partition_ids,
-    })
+    Ok(())
 }
 
 pub async fn execute_create_consumer_group(
diff --git a/core/server/src/shard/handlers.rs b/core/server/src/shard/handlers.rs
index 505eb50f1..5d4de789c 100644
--- a/core/server/src/shard/handlers.rs
+++ b/core/server/src/shard/handlers.rs
@@ -152,10 +152,8 @@ async fn handle_request(
                 "CreatePartitionsRequest should only be handled by shard0"
             );
 
-            let result = execution::execute_create_partitions(shard, user_id, command).await?;
-            Ok(ShardResponse::CreatePartitionsResponse(
-                result.partition_ids,
-            ))
+            execution::execute_create_partitions(shard, user_id, command).await?;
+            Ok(ShardResponse::CreatePartitionsResponse)
         }
         ShardRequestPayload::DeletePartitionsRequest { user_id, command } => {
             assert_eq!(
@@ -163,10 +161,8 @@ async fn handle_request(
                 "DeletePartitionsRequest should only be handled by shard0"
             );
 
-            let result = execution::execute_delete_partitions(shard, user_id, command).await?;
-            Ok(ShardResponse::DeletePartitionsResponse(
-                result.partition_ids,
-            ))
+            execution::execute_delete_partitions(shard, user_id, command).await?;
+            Ok(ShardResponse::DeletePartitionsResponse)
         }
         ShardRequestPayload::CreateStreamRequest { user_id, command } => {
             assert_eq!(
@@ -201,8 +197,8 @@ async fn handle_request(
                 "DeleteTopicRequest should only be handled by shard0"
             );
 
-            let result = execution::execute_delete_topic(shard, user_id, command).await?;
-            Ok(ShardResponse::DeleteTopicResponse(result.topic_id))
+            execution::execute_delete_topic(shard, user_id, command).await?;
+            Ok(ShardResponse::DeleteTopicResponse)
         }
         ShardRequestPayload::CreateUserRequest { user_id, command } => {
             assert_eq!(
@@ -240,8 +236,8 @@ async fn handle_request(
                 "DeleteStreamRequest should only be handled by shard0"
             );
 
-            let result = execution::execute_delete_stream(shard, user_id, command).await?;
-            Ok(ShardResponse::DeleteStreamResponse(result.stream_id))
+            execution::execute_delete_stream(shard, user_id, command).await?;
+            Ok(ShardResponse::DeleteStreamResponse)
         }
         ShardRequestPayload::UpdatePermissionsRequest { user_id, command } => {
             assert_eq!(
diff --git a/core/server/src/shard/transmission/frame.rs b/core/server/src/shard/transmission/frame.rs
index ab4351011..a89a34617 100644
--- a/core/server/src/shard/transmission/frame.rs
+++ b/core/server/src/shard/transmission/frame.rs
@@ -73,15 +73,15 @@ pub enum ShardResponse {
     },
     Event,
     CreateStreamResponse(StreamResponseData),
-    DeleteStreamResponse(usize),
+    DeleteStreamResponse,
     CreateTopicResponse(TopicResponseData),
     UpdateTopicResponse,
-    DeleteTopicResponse(usize),
+    DeleteTopicResponse,
     CreateUserResponse(User),
     DeleteUserResponse(User),
     GetStatsResponse(Stats),
-    CreatePartitionsResponse(Vec<usize>),
-    DeletePartitionsResponse(Vec<usize>),
+    CreatePartitionsResponse,
+    DeletePartitionsResponse,
     UpdateStreamResponse,
     SocketTransferResponse,
     UpdatePermissionsResponse,
diff --git a/core/server/src/tcp/connection_handler.rs b/core/server/src/tcp/connection_handler.rs
index d3a58db57..d80835d46 100644
--- a/core/server/src/tcp/connection_handler.rs
+++ b/core/server/src/tcp/connection_handler.rs
@@ -16,22 +16,20 @@
  * under the License.
  */
 
-use crate::binary::command;
-use crate::binary::command::ServerCommandHandler;
+use crate::binary::dispatch::{self, HandlerResult, MAX_CONTROL_FRAME_PAYLOAD};
 use crate::server_error::ConnectionError;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
-use crate::tcp::connection_handler::command::ServerCommand;
 use async_channel::Receiver;
 use bytes::BytesMut;
 use futures::FutureExt;
+use iggy_binary_protocol::RequestFrame;
+use iggy_binary_protocol::codes::{SEND_MESSAGES_CODE, command_name};
 use iggy_common::{GET_CLUSTER_METADATA_CODE, IggyError, SenderKind};
 use std::io::ErrorKind;
 use std::rc::Rc;
 use tracing::{debug, error, info};
 
-const INITIAL_BYTES_LENGTH: usize = 4;
-
 /// Connection lifecycle action after command handling.
 pub enum ConnectionAction {
     /// Continue handling connection on current shard.
@@ -47,14 +45,13 @@ pub(crate) async fn handle_connection(
     shard: &Rc<IggyShard>,
     stop_receiver: Receiver<()>,
 ) -> Result<ConnectionAction, ConnectionError> {
-    let mut length_buffer = BytesMut::with_capacity(INITIAL_BYTES_LENGTH);
-    let mut code_buffer = BytesMut::with_capacity(INITIAL_BYTES_LENGTH);
+    let mut header_buffer = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
     loop {
-        let read_future = sender.read(length_buffer);
+        let read_future = sender.read(header_buffer);
         // TODO(hubcio): this futures::select! call is translated to epoll_wait syscall for every
         // message, which adds around 100 us median latency. We could instead just call sender.shutdown()
         // if some atomic bool is set, since this is all happenng within single thread.
-        let (_, mut initial_buffer) = futures::select! {
+        let (_, mut header_buf) = futures::select! {
             _ = stop_receiver.recv().fuse() => {
                 info!("Connection stop signal received for session: {}", session);
                 let _ = sender.send_error_response(IggyError::Disconnected).await;
@@ -62,9 +59,9 @@ pub(crate) async fn handle_connection(
             }
             result = read_future.fuse() => {
                 match result {
-                    (Ok(read_length), initial_buffer) => (read_length, initial_buffer),
-                    (Err(error), initial_buffer) => {
-                        length_buffer = initial_buffer;
+                    (Ok(_), buf) => (Ok::<(), IggyError>(()), buf),
+                    (Err(error), buf) => {
+                        header_buffer = buf;
                         if error.as_code() == IggyError::ConnectionClosed.as_code() {
                             return Err(ConnectionError::from(error));
                         } else {
@@ -77,38 +74,55 @@ pub(crate) async fn handle_connection(
             }
         };
 
-        let length =
-            u32::from_le_bytes(initial_buffer[0..INITIAL_BYTES_LENGTH].try_into().unwrap());
-        let (res, mut code_buffer_out) = sender.read(code_buffer).await;
-        res?;
-        let code: u32 =
-            u32::from_le_bytes(code_buffer_out[0..INITIAL_BYTES_LENGTH].try_into().unwrap());
-        initial_buffer.clear();
-        code_buffer_out.clear();
-        length_buffer = initial_buffer;
-        code_buffer = code_buffer_out;
-        debug!("Received a TCP request, length: {length}, code: {code}");
-        let command = ServerCommand::from_code_and_reader(code, sender, length - 4).await?;
-        debug!("Received a TCP command: {command}, payload size: {length}");
-        let cmd_code = command.code();
-        match command.handle(sender, length, session, shard).await {
+        let length = u32::from_le_bytes(header_buf[0..4].try_into().unwrap());
+        let code = u32::from_le_bytes(header_buf[4..8].try_into().unwrap());
+        header_buf.clear();
+        header_buffer = header_buf;
+
+        let cmd_name = command_name(code).unwrap_or("unknown");
+        debug!("Received a TCP request, length: {length}, code: {code} ({cmd_name})");
+
+        let payload_length = match RequestFrame::payload_length(length) {
+            Ok(len) => len,
+            Err(_) => {
+                sender
+                    .send_error_response(IggyError::InvalidCommand)
+                    .await?;
+                continue;
+            }
+        };
+
+        let result = if code == SEND_MESSAGES_CODE {
+            dispatch::dispatch_send_messages(sender, payload_length, session, shard).await
+        } else {
+            if payload_length > MAX_CONTROL_FRAME_PAYLOAD {
+                sender
+                    .send_error_response(IggyError::InvalidCommand)
+                    .await?;
+                continue;
+            }
+            let payload = dispatch::read_payload(sender, payload_length).await?;
+            let frame = RequestFrame::from_parts(code, &payload);
+            dispatch::dispatch(frame, sender, session, shard).await
+        };
+
+        match result {
             Ok(handler_result) => match handler_result {
-                command::HandlerResult::Finished => {
+                HandlerResult::Finished => {
                     debug!(
-                        "Command {cmd_code} was handled successfully, session: {session}. TCP response was sent."
+                        "Command {code} ({cmd_name}) was handled successfully, session: {session}. TCP response was sent."
                     );
                 }
-                command::HandlerResult::Migrated { to_shard } => {
+                HandlerResult::Migrated { to_shard } => {
                     info!(
-                        "Command {cmd_code} was transfer to shard {to_shard}, session: {session}. TCP response was sent."
+                        "Command {code} ({cmd_name}) was transferred to shard {to_shard}, session: {session}."
                     );
 
                     return Ok(ConnectionAction::Migrated { to_shard });
                 }
             },
             Err(error) => {
-                // Special handling for GetClusterMetadata when clustering is disabled
-                if cmd_code == GET_CLUSTER_METADATA_CODE
+                if code == GET_CLUSTER_METADATA_CODE
                     && matches!(error, IggyError::FeatureUnavailable)
                 {
                     debug!(
@@ -118,7 +132,7 @@ pub(crate) async fn handle_connection(
                     debug!("TCP error response was sent to: {session}.");
                 } else {
                     error!(
-                        "Command with code {cmd_code} was not handled successfully, session: {session}, error: {error}."
+                        "Command with code {code} ({cmd_name}) was not handled successfully, session: {session}, error: {error}."
                     );
 
                     if matches!(error, IggyError::ClientNotFound(_) | IggyError::StaleClient) {
diff --git a/core/server/src/websocket/connection_handler.rs b/core/server/src/websocket/connection_handler.rs
index 2ee97196f..e68bcc902 100644
--- a/core/server/src/websocket/connection_handler.rs
+++ b/core/server/src/websocket/connection_handler.rs
@@ -16,34 +16,31 @@
  * under the License.
  */
 
-use crate::binary::command;
-use crate::binary::command::ServerCommandHandler;
+use crate::binary::dispatch::{self, HandlerResult, MAX_CONTROL_FRAME_PAYLOAD};
 use crate::server_error::ConnectionError;
 use crate::shard::IggyShard;
 use crate::streaming::session::Session;
-use crate::websocket::connection_handler::command::ServerCommand;
 use async_channel::Receiver;
 use bytes::BytesMut;
 use futures::FutureExt;
+use iggy_binary_protocol::RequestFrame;
+use iggy_binary_protocol::codes::{SEND_MESSAGES_CODE, command_name};
 use iggy_common::{IggyError, SenderKind};
 use std::io::ErrorKind;
 use std::rc::Rc;
 use tracing::{debug, error, info, warn};
 
-const INITIAL_BYTES_LENGTH: usize = 4;
-
 pub(crate) async fn handle_connection(
     session: &Session,
     sender: &mut SenderKind,
     shard: &Rc<IggyShard>,
     stop_receiver: Receiver<()>,
 ) -> Result<(), ConnectionError> {
-    let mut length_buffer = BytesMut::with_capacity(INITIAL_BYTES_LENGTH);
-    let mut code_buffer = BytesMut::with_capacity(INITIAL_BYTES_LENGTH);
+    let mut header_buffer = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
 
     loop {
-        let read_future = sender.read(length_buffer);
-        let (_, mut initial_buffer) = futures::select! {
+        let read_future = sender.read(header_buffer);
+        let (_, mut header_buf) = futures::select! {
             _ = stop_receiver.recv().fuse() => {
                 info!("Connection stop signal received for session: {}", session);
                 let _ = sender.send_error_response(IggyError::Disconnected).await;
@@ -51,9 +48,9 @@ pub(crate) async fn handle_connection(
             }
             result = read_future.fuse() => {
                 match result {
-                    (Ok(read_length), initial_buffer) => (read_length, initial_buffer),
-                    (Err(error), initial_buffer) => {
-                        length_buffer = initial_buffer;
+                    (Ok(_), buf) => (Ok::<(), IggyError>(()), buf),
+                    (Err(error), buf) => {
+                        header_buffer = buf;
                         if error.as_code() == IggyError::ConnectionClosed.as_code() {
                             return Err(ConnectionError::from(error));
                         } else {
@@ -66,63 +63,80 @@ pub(crate) async fn handle_connection(
             }
         };
 
-        let length =
-            u32::from_le_bytes(initial_buffer[0..INITIAL_BYTES_LENGTH].try_into().unwrap());
-        let (res, mut code_buffer_out) = sender.read(code_buffer).await;
-        res?;
-        let code: u32 =
-            u32::from_le_bytes(code_buffer_out[0..INITIAL_BYTES_LENGTH].try_into().unwrap());
-        initial_buffer.clear();
-        code_buffer_out.clear();
-        length_buffer = initial_buffer;
-        code_buffer = code_buffer_out;
+        let length = u32::from_le_bytes(header_buf[0..4].try_into().unwrap());
+        let code = u32::from_le_bytes(header_buf[4..8].try_into().unwrap());
+        header_buf.clear();
+        header_buffer = header_buf;
+
+        let cmd_name = command_name(code).unwrap_or("unknown");
+        debug!("Received a WebSocket request, length: {length}, code: {code} ({cmd_name})");
+
+        let payload_length = match RequestFrame::payload_length(length) {
+            Ok(len) => len,
+            Err(_) => {
+                sender
+                    .send_error_response(IggyError::InvalidCommand)
+                    .await?;
+                continue;
+            }
+        };
 
-        debug!("Received a WebSocket request, length: {length}, code: {code}");
-        let command = ServerCommand::from_code_and_reader(code, sender, length - 4).await?;
-        debug!("Received a WebSocket command: {command}, payload size: {length}");
+        let result = if code == SEND_MESSAGES_CODE {
+            dispatch::dispatch_send_messages(sender, payload_length, session, shard).await
+        } else {
+            if payload_length > MAX_CONTROL_FRAME_PAYLOAD {
+                sender
+                    .send_error_response(IggyError::InvalidCommand)
+                    .await?;
+                continue;
+            }
+            let payload = dispatch::read_payload(sender, payload_length).await?;
+            let frame = RequestFrame::from_parts(code, &payload);
+            dispatch::dispatch(frame, sender, session, shard).await
+        };
 
-        match command.handle(sender, length, session, shard).await {
-            Ok(_) => {
+        match result {
+            Ok(HandlerResult::Finished) => {
                 debug!(
-                    "Command was handled successfully, session: {session}. WebSocket response was sent."
+                    "Command {code} ({cmd_name}) was handled successfully, session: {session}. WebSocket response was sent."
                 );
             }
-            Err(error) => {
-                match error {
-                    IggyError::TcpError | IggyError::ConnectionClosed | IggyError::Disconnected => {
-                        warn!(
-                            "Client {} closed connection during request processing",
-                            session.client_id
-                        );
-                        return Err(ConnectionError::from(IggyError::ConnectionClosed));
-                    }
-                    IggyError::ClientNotFound(_) | IggyError::StaleClient => {
-                        error!("Command failed for session: {session}, error: {error}.");
-                        sender.send_error_response(error.clone()).await?;
-                        return Err(ConnectionError::from(error));
-                    }
-                    _ => {
-                        error!("Command failed for session: {session}, error: {error}.");
-                        // try to send error response, but if it fails due to broken pipe, that's ok?
-                        match sender.send_error_response(error).await {
-                            Ok(_) => {
-                                debug!("WebSocket error response was sent to: {session}.");
-                            }
-                            Err(IggyError::ConnectionClosed) => {
-                                warn!(
-                                    "Could not send error response to {} - client already disconnected",
-                                    session.client_id
-                                );
-                                return Err(ConnectionError::from(IggyError::ConnectionClosed));
-                            }
-                            Err(send_err) => {
-                                error!("Failed to send error response: {send_err}");
-                                return Err(ConnectionError::from(send_err));
-                            }
+            Ok(HandlerResult::Migrated { to_shard }) => {
+                warn!("Unexpected migration on WebSocket: to_shard {to_shard}, session: {session}");
+            }
+            Err(error) => match error {
+                IggyError::TcpError | IggyError::ConnectionClosed | IggyError::Disconnected => {
+                    warn!(
+                        "Client {} closed connection during request processing",
+                        session.client_id
+                    );
+                    return Err(ConnectionError::from(IggyError::ConnectionClosed));
+                }
+                IggyError::ClientNotFound(_) | IggyError::StaleClient => {
+                    error!("Command failed for session: {session}, error: {error}.");
+                    sender.send_error_response(error.clone()).await?;
+                    return Err(ConnectionError::from(error));
+                }
+                _ => {
+                    error!("Command failed for session: {session}, error: {error}.");
+                    match sender.send_error_response(error).await {
+                        Ok(_) => {
+                            debug!("WebSocket error response was sent to: {session}.");
+                        }
+                        Err(IggyError::ConnectionClosed) => {
+                            warn!(
+                                "Could not send error response to {} - client already disconnected",
+                                session.client_id
+                            );
+                            return Err(ConnectionError::from(IggyError::ConnectionClosed));
+                        }
+                        Err(send_err) => {
+                            error!("Failed to send error response: {send_err}");
+                            return Err(ConnectionError::from(send_err));
                         }
                     }
                 }
-            }
+            },
         }
     }
 }