Design Patterns in Rust: When and Why to Use Them

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Design Patterns in Rust: Not Just a Trend, But a Necessity



When we talk about Rust, many people think of the type system, borrow checker, and the absence of a garbage collector. But behind this facade of strictness lies a powerful tool for architecture design. Design patterns in Rust are not just a tribute to Java or C++ traditions. They are a way to make the compiler work for you, not against you.



In this article, we will explore why the classic GoF (Gang of Four) patterns look different in Rust, which ones are truly useful, and which are antipatterns. And most importantly, we will see real code that you can immediately apply in your project.



Why Does Rust Change the Game?



Classic design patterns often solve problems related to memory management, inheritance, and polymorphism. Rust offers a completely different approach:



  • Ownership and Borrowing — instead of garbage collection, you explicitly manage memory. Patterns that in other languages solve the "who owns the object" problem become trivial or unnecessary in Rust.
  • Traits — a replacement for inheritance. Rust does not support classic OOP inheritance, but traits provide flexibility without the diamond problem.
  • Enums and Pattern Matching — a powerful tool that replaces many patterns related to state and branching.
  • Functional Style — iterators, closures, and combinators allow you to write concise code without explicit patterns.


Therefore, many patterns in Rust look not like "class and interface," but like trait + struct + enum. Let's look at specific examples.



Builder Pattern: Constructing Complex Objects



The Builder pattern is one of the most popular in Rust. It is ideal for creating objects with a large number of parameters, especially when some of them are optional.



Problem: A Constructor with 10 Parameters



Imagine you need to create an HTTP client configuration. In the traditional approach, you would write:



let client = HttpClient::new(    "https://api.example.com",    Duration::from_secs(30),    5,    true,    None,    vec!["Accept: application/json".to_string()],    ...);


This is unreadable and it's easy to get the argument order wrong. The Builder solves this problem.



Implementation in Rust



In Rust, the Builder pattern is implemented using a separate struct with setter methods that return self:



#[derive(Debug)]pub struct HttpClient {    base_url: String,    timeout: Duration,    max_retries: u32,    verify_ssl: bool,    headers: Vec<String>,}

pub struct HttpClientBuilder { base_url: String, timeout: Duration, max_retries: u32, verify_ssl: bool, headers: Vec<String>,}

impl HttpClientBuilder { pub fn new(base_url: &str) -> Self { HttpClientBuilder { base_url: base_url.to_string(), timeout: Duration::from_secs(30), max_retries: 3, verify_ssl: true, headers: Vec::new(), } }

pub fn timeout(mut self, duration: Duration) -> Self { self.timeout = duration; self }

pub fn max_retries(mut self, n: u32) -> Self { self.max_retries = n; self }

pub fn no_ssl(mut self) -> Self { self.verify_ssl = false; self }

pub fn add_header(mut self, header: &str) -> Self { self.headers.push(header.to_string()); self }

pub fn build(self) -> Result<HttpClient, String> { if self.base_url.is_empty() { return Err("base_url cannot be empty".to_string()); } Ok(HttpClient { base_url: self.base_url,

Blogs

Book Recommendations