DEV Community

Illia
Illia

Posted on

From 'Why the F@&k Do I Need This?' to 'Oh, That's Why' - My GAT Journey

"Understanding GATs isn't about memorizing syntax - it's about that moment when you finally see WHY they exist."

Anyone can find the knowledge needed to just get the essence of what Async Rust, or some of the advanced types are all about. But what's the actual process of truly understanding them like?
It's different for everyone - and for that, let me share with you how it went so far from my perspective.

The Journey

Before I even started the RBES (Rust Blockchain & Embedded Systems) curriculum - you have to keep in mind that I didn't just find the passion to start learning Rust from scratch out of thin air.
No no - I actually learned quite a few other languages to test out my passion for programing - and that includes JavaScript / TypeScript, Golang, Swift (where all started because I wanted to be in the Apple ecosystem).

When I heard about Rust - the whole idea of stringent memory safety, low-level programming attracted me to get to know it more - and it was hyped quite a bunch, so I decided to grab
the Rust book in the official site and to just have a crack at it.

After SIX iterations of the curriculum design (took several months, now that I remember) - I can proudly say I am now at Week 5 of the RBES program where the prior weeks
are synthesized to fully solidify the material covered.

What those weeks covered, you might ask?

Well, let me just briefly go through them:

  • Week 1 + 2 Async Rust - Fundamentals

I didn't just use Tokio - I built a mini runtime with Pin, Poll, and custom wakers to understand what .await actually does under the hood.

On top of that, I covered the importance of Arc and Mutex and passing resources over the Tokio-powered channels such as mpsc -
and the underlying marker traits that allow us to work in the async logic safely - the Send and Sync marker traits

  • Week 3 + 4: GATs + HRTBs + PhantomTypes

Here, we got to know what those types are, how they are different from, say, a normal trait bonud in a type defintion or in a function signature - that allows us to write more flexible
APIs, deal with several lifetimes, avoiding runtime costs and allocating much less data to the heap.

Check my Week 5 progress to get a better understanding of how that learnign process went (spolier alert: NOT as quick and smooth as it sounded theoretically).

  • Week 5 (where we're at)

All of the above material has been brought together to attempt to solidify the knowledge accumulated before
moving forward to something a little bit scarier but a whole lot more exciting:

Unsafe Rust!

This is the week where I got to further strengthen my understanding of, primarily, Week 3 + 4 material, namely the GATs.
And I thought that in the beginning of Week 3 I understood what GATs' true power is - boy was I wrong...

Let me just illustrate briefly how my concept knowledge went from Week 3 to Week 5:

  • GATs: "We can work with generics and lifetimes when design a more expressive trait with more flexible associated types" -> "GATs allow us to
    returned borrowed values into the implementor's data as long as the implementor is alive - no memory allocaiton is done, whereas with a normal associated type -> the borrow is released when you return an owned associated type value"

  • HRTBs: That pretty much stayed the same - we work with a function that take a closure (or a generic by itself) that can work with ANY lifetime the caller decides on - per each call.

  • PhantomTypes (the easiest for me) - An efficient tool that utilizes zero-size data types for building state machines that validate type states at compile-time.

That bit went from "I'm not sure why they are necessary in real code", along with "why the f**k you gave a me a task to use GAT when you told me afterwards it was not needed?!", all the way to
"Here is my idea of how a GAT could be useful" (I will show you that example in just a bit).

But first... Let's talk about some of my [modest] portfolio highlights.

Portfolio Highlights

1. Async Payment Processor - The GAT "Aha" Moment

This is where GATs stopped being a theoretical concept and became a necessary tool.
The task was simple on the surface: build a payment processor that could handle transactions asynchronously. But here's the catch - I needed to return borrowed data from the processor's internal state without cloning it on every operation.

The Technical Challenge:

Without GATs, I had two bad options:

  • Clone everything: Return owned Strings and Vecs, which means heap allocations on every transaction query. When you're processing thousands of payments, every clone is latency you can't afford.

  • Use lifetimes everywhere: But then every caller needs to manage lifetimes explicitly, making the API a nightmare to use.

The GAT Solution:

pub trait AsyncPaymentProcessor {
    type TransactionData<'a> where Self: 'a;

    async fn get_transaction<'a>(&'a self, id: TxId) -> Option<Self::TransactionData<'a>>;
}
Enter fullscreen mode Exit fullscreen mode

That 'a parameter in the associated type? That's the magic. It says: "I'm returning borrowed data that lives as long as the processor does." The caller gets a reference tied to self's lifetime - no clones, no unnecessary heap allocations.

What This Proves:

I can identify when zero-cost abstractions actually matter. In systems where performance isn't just nice-to-have (blockchain consensus, game engines, real-time trading), understanding the difference between "elegant code" and "efficient code" is the difference between shipping and failing.

2. Mini Async Runtime - Understanding What .await Actually Does

Anyone can use Tokio. I built my own minimal runtime to understand what happens under the hood.

The Core Insight:

The biggest "aha" wasn't about Pin or Poll syntax - it was understanding the handoff dance between futures and the IO driver:

  1. Future yields: "I'm waiting on network IO, here's my waker handle"

  2. Executor parks it: Future goes into a HashMap (not a queue!)

  3. OS completes IO: Driver receives notification

  4. Waker activates: Driver looks up the TaskId and wakes that specific future

  5. Executor re-polls: Future resumes exactly where it left off

Why HashMap vs VecDeque Matters:

When an IO operation completes, the OS needs to wake that specific future, not scan through a queue. HashMap lookup by TaskId is O(1) - this is how real async runtimes scale to thousands of concurrent tasks without wasting CPU cycles polling futures that aren't ready.

What This Proves:

I don't just use abstractions - I understand their implementation trade-offs. When debugging production async code or optimizing hot paths, knowing why the runtime makes certain choices means I can write code that works with the system, not against it.

3. HTTP Request Builder - Compile-Time State Machines

This one showcases phantom types and the builder pattern, but more importantly, it demonstrates how Rust's type system can prevent entire classes of bugs at compile time.

The Design:

struct Request {
    url: String,
    headers: HashMap,
    _method: PhantomData,
    _state: PhantomData,
}
Enter fullscreen mode Exit fullscreen mode

Those PhantomData markers don't exist at runtime (zero-size types), but at compile time they enforce state transitions:

  • Request can call .with_header()

  • Request can call .with_body()

  • Request can call .send()

You cannot call .send() on a request without headers. Not "you shouldn't" - you cannot. The compiler won't let you compile.

Why This Matters:

When you're building APIs at scale - whether it's a blockchain RPC client or a game's network layer - catching state machine errors at compile-time means your users can't misuse your API. It's the difference between "safe by convention" (documentation saying "don't do this") and "safe by construction" (the type system enforcing it).

What This Proves:

I can design APIs that are both ergonomic (builder pattern feels natural) and bulletproof (invalid states are unrepresentable). This is the kind of API design that matters when mistakes are expensive - and in blockchain or embedded systems, mistakes are expensive.

The GAT Breakthrough (Week 4-5)

The Setup: Confidence Before the Fall

Week 3, I learned about GATs. I understood the syntax. I could explain that they're "generic associated types that let you add lifetime parameters." I thought I got it.
Week 4, you (Claude) gave me a task: "Build an HTTP request builder using phantom types. Find a way to incorporate GATs."

Cool. I'd mastered GATs, right? Let's do this.

The Struggle: When Theory Meets Reality

I tried everything to make GATs work with the state machine:

trait BuilderStep {
    type Next<S>;  // GAT for state transitions!
    fn next_step(self) -> Self::Next<S>;
}
Enter fullscreen mode Exit fullscreen mode

But wait... if the GAT method gets to decide the return type based on the generic parameter S, how do I enforce that .with_header() must return HeadersSet state?
The whole point of phantom types is compile-time guarantees.
If the method itself is generic over the next state, I've just opened the door to any state transition - which defeats the entire purpose of a state machine.
I kept hitting this wall. The compiler would accept my code, but it was wrong. Too flexible. No guarantees.

The Realization: Not Every Problem Needs a Hammer

After what felt like hours of wrestling with syntax, you said something that changed how I think about these tools:

"GATs aren't for strict type transitions. They're for when you need lifetime flexibility or borrowing from self."

Oh.

Oh.

GATs weren't wrong for state machines because I was bad at using them. They were wrong because that's not what they're for.

The HTTP builder needs specific, enforced transitions:

pub struct Request<Method, State> {
    url: String,
    headers: HashMap<String, String>,
    body: Option<String>,
    _method: PhantomData<Method>,
    _state: PhantomData<State>,
}

impl<M> Request<M, Initialized> {
    pub fn new(url: impl Into<String>) -> Self {
        // Your implementation
        Self {
            url: url.into(),
            headers: HashMap::new(),
            body: None,
            _method: PhantomData,
            _state: PhantomData,
        }
    }

    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Request<M, HeadersSet> {
        self.headers.insert(key.into(), value.into());
        Request {
            url: self.url,
            headers: self.headers,
            body: self.body,
            _method: PhantomData,
            _state: PhantomData,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

No GAT. No lifetime parameter. Just: "This state goes to this state, period."

The Proof: Finding Where GATs Actually Belong

Then came the async payment processor.

This time, the problem was different: I needed to return borrowed data from the processor's internal state - transaction records, account balances - without cloning them on every query.

trait AsyncPaymentProcessor {
    type TransactionData<'a> where Self: 'a;

    async fn get_transaction<'a>(&'a self, id: TxId) -> Option<Self::<TransactionData<'a>>>;
}
Enter fullscreen mode Exit fullscreen mode

This is where GATs shine. The lifetime 'a ties the returned data to how long self is borrowed.
The caller can hold a reference as long as they're holding the processor - no clones, no heap allocations, no unnecessary overhead.

Without GATs? I'd be returning owned Strings and Vecs on every transaction query. With thousands of concurrent transactions, that's latency I can't afford.

The Lesson: Judgment Over Memorization

The breakthrough wasn't learning GAT syntax - it was developing engineering judgment:

  • Phantom types: When you need compile-time state enforcement with zero runtime cost

  • GATs: When you need to return borrowed data with flexible lifetimes tied to self

  • Regular associated types: When you just need type-level abstraction without borrowing

It's not about which tool is "better" - it's about recognizing which problem you're actually solving.

And honestly? That's the lesson I wish I'd understood in Week 3.
But struggling through the HTTP builder, being wrong, getting frustrated, and then building the payment processor with the right tool for the right job - that's what made it stick.

Anyone can memorize syntax. Understanding why a feature exists? That takes bruises.Technical Deep Dives

Week 1-2: Async Rust - From Surface to System

Before the RBES curriculum, I thought async Rust was just async/.await syntax and maybe using mpsc channels for message passing. That's it. I'd read the Rust book's async section, understood the surface layer because I had JavaScript experience, and figured that was enough.

I was painfully wrong.

What Changed:

Now I understand the entire runtime executor cycle - how futures are polled, how the IO driver signals the waker when operations complete, how the executor knows which specific future to re-poll. It's not magic syntax - it's a coordinated dance between the executor, futures, and the OS.

What I Can Explain Confidently:

  • Async functions are lazy until polled (via .await, tokio::join!, tokio::select!, etc.)

  • The Send/Sync marker traits and when resources need to be wrapped in Arc> for multi-threaded safety

  • That TCP server handling 500 concurrent connections? I can explain why the mpsc receiver needed Arc>for multiple workers to grab tasks from a single channel

What I Don't Know Yet:

Honestly? I won't know what I need to look up until I hit a project with requirements beyond basic async patterns. The async_trait macro for the payment processor was a perfect example - completely new territory. That's how learning works: you don't know the gaps until you need to fill them.

Week 3-4: Type System - Bruises Build Understanding

Difficulty Ranking (Hardest → Easiest):

  1. HRTBs - Even the official docs admit explicit HRTBs are rare (despite recent lifetime elision improvements in Rust 1.85+). I haven't had enough real-world experience with them because needing to handle multiple inputs' lifetimes (or no lifetimes at all) just doesn't come up often.

  2. GATs - Still not trivial, but the bruises from Week 5 made them stick. Applying them wrong (HTTP builder), then applying them right (async payment processor) taught me more than any documentation could.

  3. Phantom Types - Straightforward once you get the zero-cost state machine pattern. These clicked fastest for me.

The False Confidence Trap:

Week 3, first encounter with GATs: "Yeah, I get it - generic associated types with lifetime parameters."
Week 5, after the HTTP builder struggle: "Oh... I didn't get it at all."
That gap between "I understand the syntax" and "I know when and why to use this" is where Week 5 lived. The intuition that something was off in Week 3 was right - I just hadn't done the work yet.

My 2-Sentence GAT Test:

When should you use a GAT?

When you need to return borrowed data tied to the implementor's lifetime and you're not sure what exact type (generic) it should return - giving that
type flexibility to the caller of the trait method, not the trait definition itself. If you need strict type transitions (like state machines), GATs are the wrong tool.

Week 5: Consolidation - Concepts Meeting Reality

The big shift in Week 5 wasn't learning new concepts - it was seeing how async and GATs work together in real systems. Building the async payment processor forced me to combine everything: lifetime management, async traits, borrowing semantics, all in one design.

If I Could Rebuild One Project:

The TCP server. Not the basic string-printing version we built in Week 1, but a more sophisticated one using the newtype pattern and GATs to
make the connection handling more interesting. Now that I actually understand what GATs are for, I could design something that borrows connection state efficiently instead of cloning data everywhere.
That's the clearest sign of progress: not "I finished the curriculum," but "I'd do it differently now."What I Can Build Now

Current Capabilities (Proven in Portfolio):

  • Concurrent TCP servers handling hundreds of simultaneous connections with proper async runtime usage

  • Type-safe state machines using phantom types for compile-time validation (HTTP request builders, protocol state tracking)

  • Zero-allocation async APIs using GATs to return borrowed data from internal state (payment processors, data access layers)

  • Custom async patterns beyond just using Tokio - understanding why executors work the way they do

Realistically, Today:

Anything that combines the Week 1-5 concepts - basic servers, API clients, concurrent task schedulers, type-safe builders. These are components of larger systems, not full applications yet.

What I Can't Build Yet (And I'm Honest About It):

  • Production blockchain infrastructure (RPC load balancers, validator monitoring platforms, MEV optimization - that's Phase 2, Weeks 15-38)

  • Embedded systems with real hardware (STM32/ESP32 interfacing, RTOS integration, automotive CAN bus - that's Phase 3, Weeks 39-61)

  • Edge AI inference systems (on-device ML models, sensor fusion, industrial IoT - that's Phase 3, Weeks 46-60)

  • Production observability stacks (Prometheus/Grafana pipelines, distributed tracing, SRE practices - that's Phase 5, Weeks 70-71)

A better answer to "what can I build?" will come after Phase 2 (blockchain infrastructure) and Phase 3 (embedded + edge AI). For now, I can build sophisticated components - the building blocks that go into larger systems. That's not modest - that's realistic.

For Collaborators/Employers:

If you need someone to build Rust libraries with strong API design, async concurrency, and compile-time safety guarantees, I can do that.
If you need someone who's shipped production blockchain RPC infrastructure or embedded automotive systems, that's not me... yet.

I know the difference. And knowing what you don't know is just as valuable as knowing what you do.

Open for Collaboration

This is my first full technical blog post, so any feedback is genuinely appreciated - comments, corrections, questions, even just a "this resonated with me" helps me understand what's landing and what's not.

Who I'm hoping to connect with:

If you're a Rust developer (whether you're a few weeks ahead or a few years ahead), or if you're working in blockchain infrastructure, embedded systems, or edge AI and want to discuss the technical challenges in these domains, I'd love to hear from you. The best learning happens in conversation, not isolation.

What I'm not doing (yet):

I'm not seeking consulting work or commercial collaborations at this stage - I'm Week 5 of an 85-week curriculum, and I'm being realistic about where I am. As I build more substantial projects and publish more posts, you'll see those calls-to-action appear naturally. For now, I'm just documenting the journey and inviting technical discussion.

Transparency note:

My curriculum is commercially focused (blockchain infrastructure + embedded systems + edge AI), so by Month 18-24,
you'll absolutely see me positioning for client work. But I'd rather be honest about being in the learning phase than oversell capabilities I haven't proven yet.

If you want to follow along, you can find my projects on GitHub, reach me on 
Discord, or connect on Reddit.
I'm documenting everything - the wins, the bruises, the "oh NOW I get it" moments.

Because understanding GATs isn't about memorizing syntax - it's about that moment when you finally see why they exist.

And I'm still collecting those moments.

Top comments (0)