Deep Engineering #47: Evan Williams on Why Experienced Developers Have the Hardest Time Learning Rust
On the borrow checker as a design tool, the object-oriented trap, and why the engineers who struggle most with Rust are often the most experienced ones
Eval Driven Development for Engineers
This hands-on workshop teaches you to build reliable, production-ready AI systems using eval-driven development. Taught by Imran Ahmad, Data Scientist and author of 50 Algorithms Every Programmer Should Know.
🗓️ May 30 · 11:00 AM – 3:30 PM ET
Use code EDD50 for 50% off.
✍️ From the editor’s desk,
Welcome to the 47th issue of Deep Engineering!
Debian’s APT package manager is moving toward the Rust threshold its maintainers set more than six months ago. APT maintainer Julian Andres Klode announced on the Debian developer mailing list that hard Rust dependencies and Rust code would be introduced no earlier than May 2026, citing memory safety and stronger unit testing as reasons to move core parsing and signature-verification paths toward Rust and the Sequoia ecosystem. For a tool that underpins Debian, Ubuntu, and their many derivatives, this is not a marginal adoption story. It is Rust moving into infrastructure that enormous numbers of systems rely on every day.
That kind of decision does not get made because Rust is fashionable. It gets made because the language can shift whole classes of errors from production failures into compile-time constraints. That is precisely the argument Evan Williams, senior software engineer and author of Design Patterns and Best Practices in Rust, makes in this week’s issue. We spoke with Williams about what it actually takes to think in Rust, why the borrow checker is a design tool rather than a compiler obstacle, and why he found it harder to write bad Rust than good Rust when working on the book. You can watch our interview or read the full Q&A here.
Let’s get started.
Featured Newsletter: DevOps Bulletin
If you work across DevOps, Cloud Native, AI and security and want a weekly read that surfaces the most relevant open-source tooling, stories, and insights in the space, DevOps Bulletin is worth adding to your reading list.
Expert Insights
Rust Makes It Harder to Write Bad Code Than Good Code
by Saqib Jan with Evan Williams
Most engineers who struggle with Rust describe the same experience. The compiler rejects code that would compile without complaint in C++ or Java, and the borrow checker surfaces errors that feel arbitrary until, gradually, they start to feel like something else entirely. Evan Williams, author of Design Patterns and Best Practices in Rust (Packt), has a precise name for what they are.
They are design feedback, not feedback about syntax or style, but about the structure of the program itself, the shape of data flow, the discipline of ownership, and the decisions about who controls what and for how long. “Rust is your partner in doing that,” Williams says. “You can still write code with bugs in it, but Rust makes it harder to do that and easier to write code that’s going to be solid.”
That framing changes how to think about the borrow checker, and it changes how to think about what Rust actually is. Most languages make it easy to write code that works in isolation. Rust makes it hard to write code that fails in combination, and the difference matters more as systems grow.
The borrow checker is enforcing design, not syntax
Most engineers who pick up Rust treat borrow checker errors as obstacles to route around. The instinct is understandable because in Java or Python, the path from a failing compiler error to working code runs through adjustment: find what the compiler dislikes, change it, move on. Rust works differently, and engineers who apply the same strategy find that routing around the borrow checker is possible in the short term and damaging in the long term.
“The borrow checker is your friend because it prevents you from making a messy design. It prevents you from making a broken design. It prevents you from writing whole classes of bugs that you will then spend many hours trying to find,” Williams says. “I have found it to be an incredible partner in writing code that allows me to sleep at night.”
The reason the borrow checker behaves this way is structural. Java and Python allow data to be accessed from many places at once, which gives engineers flexibility but leaves the responsibility of managing that access entirely with the programmer. Rust removes that flexibility. A value has one owner. References are either shared and immutable or exclusive and mutable, never both at the same time. This constraint forces the programmer to be explicit about who owns what and when, because the compiler will not let the program proceed otherwise. The practical consequence is that programs which compile in Rust tend to have a quality that programs in other languages achieve only through discipline: their data flows are explicit. You can read a Rust program and understand, without running it, who controls which piece of state, when that control transfers, and what happens at the boundary.
“The principles that the borrow checker forces you to adhere to in Rust are the exact principles that you should be using in every programming language,” Williams reasons. “But you don’t have to. So it’s very easy to not think about those things.”
The object-oriented trap
The single most common mistake engineers make when getting started with Rust is treating it as an object-oriented language. It resembles one superficially, with structs, methods, and something that looks like encapsulation, but it has no inheritance, no abstract base classes, and no shared mutable state by default. An engineer who brings a Java or C++ mental model will find that the things they reach for instinctively are either unavailable or actively counterproductive.
“If you carry with you an object-oriented language mindset, then you’re going to have nothing but trouble,” Williams says. “The more experienced you are, the more years you have doing something in some other language, the more trouble you’re likely to have, because you have patterns of thought that come from those languages that you don’t even realize are there.”
This is a precise observation about how expertise transfers, or fails to. An engineer with ten years in Java has a large inventory of solutions to common problems, and most of those solutions depend on inheritance, shared mutable references, or runtime polymorphism through interfaces. In Rust, none of those approaches work as expected. The patterns are not wrong in their original context. They are wrong for this one, and the difficulty is that the engineer applying them does not recognize the mismatch until the borrow checker makes it unavoidable.
The design patterns that experienced engineers carry into Rust need to be examined before use, not applied by default. Some evolve into new forms because Rust’s enums and advanced generics make several classical patterns either less necessary or unnecessary entirely. Others require fundamental rethinking. The Singleton pattern, useful enough in Java and Python that engineers reach for it without deliberation, tends to become either redundant or actively problematic in Rust. “In Rust, it tends to be either completely unnecessary because other features of the language make it unneeded, or it tends to encourage designs that are really not necessary and where a much better approach could be used,” Williams says.
The replacement for inheritance in most cases is traits, which provide polymorphism without the coupling that comes from sharing a class hierarchy. The discipline required to work with traits well is the same discipline the borrow checker enforces on data: think about the boundaries, be explicit about what crosses them, and design the structure before writing the code.
Ownership as architecture
The ownership model does more than prevent bugs at the function level. It shapes the architecture of the system, because the rules that apply to individual values apply at every level of scale. A program that handles data ownership correctly in a single function has to handle it correctly across modules, across threads, and across the boundaries between components. The borrow checker enforces this at compile time, which means architectural decisions that in other languages can be deferred until the system grows large enough to break start being made from the beginning.
“You need to think about who controls what, how it is controlled, and you need to start from the very beginning thinking about the boundaries of your program and the system architecture, dividing things up into areas of responsibility,” Williams says. “Because unlike Python or Java, you can’t have links going all over the place. The borrow checker is never going to accept that.”
This constraint produces a specific architectural tendency in well-written Rust systems: data flows in one direction. Rather than components that hold references to each other in a web of mutual dependency, Rust systems tend toward chains of ownership that move in one direction and do not loop back. “By saying, I have a chain of ownership that moves down but never moves back up, you are now much more likely to have a system that is going to work,” Williams says. “Data flowing down is something that feels natural and smooth and just works. Data trying to fight the stream back up is going to end up giving you problems because the borrow checker is not going to like you.”
The architectural benefit of this tendency is legibility as much as correctness. A system where data flows in one direction is a system where behavior is predictable from the structure, and debugging does not require reconstructing who might have modified a value and when, because the ownership model makes that history explicit.
Williams illustrates this with an example from his book, a miniature publish-and-subscribe system built to resemble Kafka at a much smaller scale. “Because Rust has move semantics, you know that if something leaves here and goes here, it’s now not here anymore. It’s there. There’s no question about things like having references dangling or anything like that. The clarity of things moving through the system, the clarity of being able to have immutable data in a lot of places and knowing who can and can’t modify any piece of data, it just makes the design of the system so clear and it makes it so much harder to make a system that doesn’t work,” he says.
The typestate pattern
The most underutilized expression of this architectural discipline in Rust is one that Williams returns to with visible enthusiasm. The typestate pattern uses the type system to encode the state of a value at compile time in a way that makes invalid state transitions not just errors but programs that will not compile.
“It’s a way of developing state machines and systems that have state that evolves where invalid state transitions aren’t just errors, they’re impossible to write. The compiler won’t compile them,” Williams says. “It represents a huge advance in the way that such systems are written because now instead of runtime errors, you have a state machine that is guaranteed to work because every transition either is a valid transition or it won’t even compile. That’s an amazing thing.”
The typestate pattern was not invented for Rust, but the language’s ownership system and its handling of types make it practical in a way that other languages do not. The result is that a class of bugs that normally surfaces at runtime, invalid transitions through a state machine, surfaces instead at compile time, before the program runs. For systems where correctness is not optional, this is a material improvement. “Not invented for Rust, but it fits Rust so perfectly, it’s hard to believe it,” Williams says.
What this requires in practice
None of this comes without a cost. The discipline that Rust enforces at compile time is discipline that engineers have to supply at design time, and for teams moving from other languages the transition is genuinely difficult. Williams is specific about where the difficulty concentrates. Velocity drops during the learning period, often enough that teams take it as a signal that the decision was wrong, and it usually is not. “Once the team becomes very well acquainted with Rust, velocity can increase dramatically, but there is a period of time where it seems like things have gotten worse,” he says.
The answer for most teams is to start with a small, non-critical piece of work rather than a rewrite of an existing system, with the goal of building familiarity in a context where the cost of roadblocks is low and then expanding from there. “What you don’t want to do is jump into saying, we’re just going to rewrite our project in Rust now. Pick a small piece, focus on that, gain confidence and mastery of the language, and then use that to build upon it and start bringing in more things,” Williams says.
There are also cases where Rust is the wrong tool. Prototyping benefits from the flexibility that Python provides and that Rust does not. Environments where the tooling is incomplete are not the right place to fight the language and the Rust ecosystem, while growing rapidly, still has gaps where Java or C libraries are well established. User interfaces are the clearest current example. But in systems where failure is expensive, where correctness cannot be approximated, and where the code has to remain understandable as the team around it changes, Rust’s constraints are not a cost. They are the point.
The harder thing to write
The most revealing observation Williams made came not from a question about patterns or architecture but from the experience of writing the early chapters of his book, the ones about what not to do. He went back and tried to write bad Rust deliberately, the kind of code that would illustrate the mistakes he was cautioning against, and it was harder than he expected.
“When I went back and tried to write bad code in Rust, it was much harder than writing the good code,” Williams says. “That’s an interesting perspective that just didn’t even occur to me.”
That observation captures something important about what the language is doing. Rust is not just a language with a strict compiler. Its constraints push code toward a specific shape, one that is explicit about data flow, deliberate about ownership, and structured around clear boundaries of responsibility. The engineers who find Rust most difficult are often the engineers with the most experience, because they have the most deeply held instincts to unlearn. And the engineers who find it most rewarding tend to be the ones who stop treating the borrow checker as an obstacle and start reading it as design feedback. The language is not rejecting their code. It is asking them to think more clearly about what the code is actually doing.
In case you missed
Here’s the full Q&A with the interview video featuring Evan Williams.
🛠️ Tool of the Week
rust-analyzer — Rust language server that provides IDE functionality for writing Rust programs.
Highlights:
Surfaces Rust diagnostics, including ownership and borrow-checker errors from compiler checks, inline during editing.
Supports major LSP-compatible editors such as VS Code, Vim, Emacs, and Zed, with regular stable releases.
Widely used across the Rust ecosystem as the standard Rust IDE backend, including in workflows at organizations that build with Rust.
📎 Tech Briefs
Dirty Frag vulnerabilities disclosed in the Linux kernel - Two CVEs in Linux ESP/IPsec and RxRPC components allow unprivileged local users to gain root on affected systems.
Linux 7.0.5 stable released with partial Dirty Frag fix - Linux 7.0.5 ships a partial XFRM/ESP patch for Dirty Frag, with a second required fix still in development at release time.
GitHub secret scanning via MCP Server now generally available - Credential scanning is now available in MCP-compatible coding agents before commits or pull requests, requiring GitHub Secret Protection to be enabled on the repository.
Linux 7.1-rc2 published — KVM selftest renaming drove the unusual patch volume in rc2, with functional work covering driver and networking fixes throughout the tree.
MySQL 9.7.0 LTS generally available - New MySQL LTS line ships the Hypergraph Optimizer in Community Edition, with Dynamic Data Masking remaining Enterprise-only in this release.
That’s all for today. Thank you so very much for reading this issue of Deep Engineering.
We’ll be back next week with more expert-led content.
Stay awesome,
Saqib Jan
Editor-in-Chief, Deep Engineering
If your company is interested in reaching an audience of senior developers, software engineers, and technical decision-makers, you may want to advertise with us.
Thanks for reading Packt Deep Engineering! Subscribe for free to receive new posts and help grow our work.





