Mad Rust: The JVM Developer's Journey
Kotlin Developer's Road to Valhalla
You’ve met them. Developers who ventured into Rust and came back... changed. They talk differently now. Ownership. Borrowing. Lifetimes. They look at your code with an expression you can’t quite read. Not judgment. Something else. Like they’ve seen things you haven’t.
You’ve been curious. Maybe a little skeptical. A language with no garbage collector that’s somehow memory-safe? A compiler so strict it catches race conditions? It sounds like a myth like stories developers tell each other.
It’s not. Rust is real, and it really does eliminate entire categories of bugs at compile time. The trade-off is that you have to think about things the JVM handled invisibly. Who owns this data. How long this reference lives. Why this code that looks fine actually isn’t.
The landscape looks almost familiar, but the rules are different. Challenging at first. Maybe even frustrating.
But everyone who makes it through says the same thing: they can’t go back. They’ve seen too much.
Ready to start?
Getting Started: Cargo Is Your New Gradle
Before we dive into syntax, let’s get familiar with Rust’s tooling. If you know Gradle or Maven, Cargo will feel right at home.
Your project structure looks like this:
The Cargo.toml file manages dependencies:
Run cargo build and Cargo fetches dependencies automatically. No separate “sync” step. Dependencies come from crates.io, Rust’s package registry.
Syntax Basics: Familiar Territory
Now for the good news. A lot of Rust syntax will feel familiar.
Variables and Mutability
Rust defaults to immutability, closer to Kotlin’s val than Java’s default:
Functions
Clean and predictable:
The last expression without a semicolon becomes the return value. Adding a semicolon turns it into a statement returning nothing (()).
Numeric Types
Rust is explicit about integer sizes:
The i means signed, u means unsigned, and the number is bit width. No platform ambiguity.
A Quick Note on Macros
You’ll notice some function calls have an exclamation mark: println!, vec!, format!. These aren’t regular functions. They’re macros.
Macros are code that generates code at compile time. They can do things regular functions can’t, like accepting a variable number of arguments or manipulating syntax. For now, just know that ! means “this is a macro.” You’ll use them constantly, and later you can learn to write your own.
The syntax basics should feel manageable. Time to see where Rust aligns with Kotlin’s strengths.
Null Safety: Option<T>
Good news. Your Kotlin null safety instincts transfer directly. Where Kotlin has String?, Rust has Option<T>:
Handling Optional Values
Pattern Matching
Rust’s match is like Kotlin’s when, but more powerful:
For simple cases, if let is more concise:
You can also add guards for additional conditions:
The pattern matching concepts will serve you well throughout Rust. Let’s look at strings next.
Strings: Two Types to Understand
Kotlin and Java have one string type. Rust has two main ones:
String: Owned, heap-allocated, growable. You own this data.&str: A borrowed view into string data.
Don’t worry too much about the difference yet. We’ll cover ownership later. For now, just know that String::from(”...”) creates a string you own.
Common Operations
Important: You can’t index strings by position (text[0]) because Rust strings are UTF-8 and characters can span multiple bytes. Use .chars():
Strings are one place where Rust’s low-level nature shows through. But once you understand ownership, the two types will make perfect sense.
Collections and Functional Programming
Standard collections map intuitively to what you know:
Functional Operations
Your Kotlin collection operations translate cleanly:
Why *x and .copied()? When you call .iter(), you get references to the values, not the values themselves. The * dereferences a reference to access the actual value. The .copied() method converts an iterator of references (&i32) into an iterator of values (i32). This relates to ownership, which we’ll cover soon.
Key difference from Kotlin: Rust iterators are lazy. Nothing happens until you call .collect() or another terminal operation like .sum().
Loops
Loops are mostly familiar territory:
The &nums syntax borrows the vector so you can still use it after the loop. Without the &, the loop would take ownership and nums would be invalid afterward. More on that soon.
Error Handling: Result Instead of Exceptions
This is your first paradigm shift. Rust has no exceptions. Functions that can fail return Result<T, E>:
You must handle the error. The compiler won’t let you ignore it.
The ? Operator
The ? operator provides concise error propagation:
The ? says: “If error, return it immediately. Otherwise, unwrap and continue.” Similar to how ?. works in Kotlin, but for errors instead of nulls.
Combining Result and Option
You can use the same pattern-matching techniques with both:
No more hunting through stack traces for uncaught exceptions. If something can fail, the type system tells you upfront.
Structs, Traits, and Enums: OOP Without Inheritance
Rust has no class inheritance. You build with composition and traits instead. At first this feels limiting, but it avoids the fragile base class problem entirely.
Structs (Your Classes)
Traits (Interfaces, But More Flexible)
Enums
Rust enums can hold data. Similar to Kotlin sealed classes:
In fact, Option and Result are just enums defined in the standard library. Nothing magical about them.
Generics
Similar to Kotlin/Java, with trait bounds instead of interface bounds:
Generics work largely as you’d expect. The syntax differs, but the concepts are the same.
You’ve now seen most of Rust’s syntax and features. If you’ve made it this far, you’re ready for the big one. The concept that makes Rust unique. The reason the compiler sometimes rejects your code.
Ownership: The Concept That Changes Everything
In Kotlin and Java, you don’t think about who “owns” an object. You create it, pass it around freely, and the garbage collector cleans up when nothing references it. Simple and automatic.
Rust has no garbage collector. Instead, it tracks ownership at compile time through three rules:
Every value has exactly one owner
When the owner goes out of scope, the value is dropped (freed)
You can have one mutable reference OR any number of immutable references, never both simultaneously
This feels restrictive until you realize what it gives you: guaranteed memory safety without runtime overhead, and freedom from data races in concurrent code. The compiler catches these bugs before your code runs.
In Kotlin, you’d use book after passing it to a function without thinking twice. In Rust, passing book to take_book moves ownership. The original variable becomes invalid. The compiler enforces this.
Borrowing: Access Without Ownership Transfer
Most of the time, you don’t want to transfer ownership. You just want a function to read or modify your data temporarily. That’s borrowing:
The & means “borrow this.” The &mut means “borrow this mutably.” The compiler ensures you never have a mutable borrow while immutable borrows exist. This prevents data races at compile time.
Mental model: Think of it like lending a physical book. Multiple friends can read it simultaneously (immutable borrows), or one friend can write notes in it (mutable borrow), but not both at the same time.
Now those & symbols you saw earlier in the article make sense. When we wrote for num in &nums, we were borrowing the vector instead of taking ownership of it. When we used *x in the filter closure, we were dereferencing a borrowed reference.
String vs &str Revisited
Remember the two string types? Now you can understand them:
Stringis an owned string. You own the memory.&stris a borrowed view into string data. You’re just looking at someone else’s memory.
This pattern of owned vs borrowed types appears throughout Rust.
A Brief Word on Lifetimes
You’ll eventually encounter syntax like this:
The ‘a is a lifetime annotation. It tells the compiler how long references are valid.
Don’t panic. You don’t need to fully understand lifetimes right now. The compiler will guide you with helpful error messages when you need them. For most code, Rust infers lifetimes automatically.
Just know that lifetimes exist to prevent dangling references (pointers to freed memory). They’re part of how Rust guarantees memory safety without a garbage collector.
When you’re ready to go deeper, the Rust Book has an excellent chapter on lifetimes.
Concurrency: Where Ownership Pays Off
Rust provides multiple approaches to concurrency. Let’s start with threads, then explore async/await, and see how they compare to what you know from Kotlin and Java.
Threads with Compile-Time Safety
The move keyword before the closure transfers ownership of sender into the closure. Without it, the closure would try to borrow sender, but the borrow might outlive the original variable.
The ownership system prevents data races at compile time. You simply cannot write code that has two threads mutating the same data without synchronization. The compiler won’t allow it.
Async/Await with Tokio
For I/O-heavy workloads like web servers, Rust offers async/await. Unlike Kotlin, Rust doesn’t include a runtime in the standard library. You choose one, and Tokio is the most popular.
First, add Tokio to your Cargo.toml:
Then you can write async code:
The #[tokio::main] macro sets up the async runtime. tokio::spawn creates concurrent tasks, and tokio::join! waits for multiple futures.
The Critical Difference: Futures Are Lazy
Here’s something that surprises every JVM developer. Rust futures do nothing until polled:
This prints “hello” then “world”. In Kotlin, async { } eagerly starts execution. In Rust, creating a future just creates a state machine. Nothing runs until you .await it.
Async Channels
Tokio provides async-aware channels for task communication:
The bounded channel provides backpressure. When the buffer is full, senders wait. This prevents memory issues under heavy load.
Comparing Rust, Kotlin, and Java Concurrency
If you’re coming from Kotlin coroutines or Java’s virtual threads, here’s how Rust’s model differs.
Rust vs Kotlin Coroutines
Rust and Kotlin both use “colored functions.” In Kotlin, suspend marks async functions. In Rust, async does the same job:
The key syntax difference: Kotlin hides await. Calling a suspending function looks like calling a regular function. Rust requires explicit .await at every suspension point. You always know exactly where your code might pause.
Structured concurrency also differs. Kotlin’s coroutineScope ensures parent-child lifecycle management:
Rust’s tokio::spawn creates detached tasks. You must explicitly join them:
Rust vs Java Virtual Threads (Project Loom)
Java 21 introduced virtual threads, and they take the opposite approach from both Rust and Kotlin. There’s no function coloring at all. Any code can be concurrent:
Virtual threads look like regular threads but are JVM-managed and incredibly lightweight. When a virtual thread blocks on I/O, the JVM automatically unmounts it and reuses the carrier thread. Your existing blocking code scales without rewrites.
The Tradeoffs
Each approach makes different design decisions.
Function coloring separates async and sync code. Rust and Kotlin both have it (async/.await and suspend respectively), which means you need async versions of libraries. Java virtual threads avoid this entirely, letting any code be concurrent without syntax changes.
Runtime requirements differ significantly. Rust requires an external runtime like Tokio. Kotlin and Java have built-in support in their standard libraries and VMs.
Memory efficiency favors Rust. Async tasks use roughly 300-400 bytes each, compared to a few kilobytes for Kotlin coroutines and 200+ bytes plus stack chunks for Java virtual threads. At massive scale, this adds up.
Blocking detection is where Rust shines. The compiler catches many concurrency bugs at compile time. Kotlin provides runtime warnings, and Java detects “pinning” events at runtime when virtual threads block inappropriately.
Migration effort varies. Moving existing code to Rust async requires rewriting. Kotlin coroutines need moderate changes. Java virtual threads often need just an executor swap, making them the easiest path for existing blocking codebases.
Safety guarantees reflect each language’s philosophy. Rust proves safety at compile time. Kotlin and Java catch issues at runtime.
When to use each:
Rust async: Maximum performance at massive scale (100K+ concurrent connections), when you need compile-time safety guarantees
Kotlin coroutines: Good balance of ergonomics and performance, excellent structured concurrency
Java virtual threads: When you have existing blocking code and want instant scalability without rewrites
When to Use Threads vs Async in Rust
Async isn’t always the answer. Use regular threads for:
CPU-bound work: Heavy computation should use threads or Rayon, not async
Simple programs: Fewer than 100 concurrent operations don’t need async complexity
Blocking I/O you can’t avoid: Use
tokio::task::spawn_blockingto offload to a thread pool
The rule of thumb: async code should never spend more than 10-100 microseconds without reaching an .await. Blocking the executor starves other tasks.
Making the Transition
Practical advice for your journey:
Trust the compiler. Its error messages are excellent. When it rejects your code, read carefully. It’s catching real bugs.
Master ownership gradually. Write small programs that move, borrow, and clone data until it becomes intuitive.
Clone liberally at first. When fighting the borrow checker,
.clone()is a valid escape hatch. Optimize later.Use
cargo clippy. It suggests more idiomatic patterns and catches common mistakes.Read The Rust Book. Free and comprehensive: doc.rust-lang.org/book
The investment pays off. Once ownership clicks, you’ll write faster, safer code and wonder why other languages let you get away with so much.
What’s Next?
This guide covered the essentials, but there’s more to explore:
Lifetimes in depth: When you start writing functions that return references, you’ll need to understand lifetime annotations. The compiler will guide you.
Advanced async patterns: We covered the basics, but there’s more:
select!for racing futures, streams for async iteration, and cancellation safety.Error handling with thiserror and anyhow: Crates that make error handling more ergonomic.
Serde: The go-to crate for serialization. Works beautifully with JSON, TOML, and many other formats.
Crates.io: Browse the ecosystem. There’s likely a crate for whatever you need.
You’ve made it through. Rust isn’t so intimidating once you understand its rules. In fact, it might just become your new favorite language.
Now go. The road to Valhalla awaits.










































