Posted on ::

I'm on a journey to become a better engineer. Not just someone who can ship code, but someone who deeply understands what's happening underneath. Someone like Tom.

This kind of comes from my own scare. A few months ago, I realized I didn't understand almost anything in my current project, even though I had onboarded without AI like a year ago. But a few months ago my company entered a deal with an AI company, and we are able to use as much AI to code as possible. Not only able, actually encouraged. I got to a point where I asked AI to do config change PRs, like "please enable this feature in QA", a literal one-liner.

When I realized I could not code on my own, I got scared. And this is the result of that scare.

AI is amazing, but it's rotting our brains

Let me be clear: AI is incredible for learning. When used properly.

The problem is that "properly" is a narrow and fuzzy window. Cross the line, and you're not learning anymore. You're just putting together code you don't understand. A Stack Overflow copy-paste on steroids, tailored to your every need (at least with Stack Overflow you had to think about how to Google and read a bit; AI pushes the code for you, you don't even have to read it).

What AI should be IMO:

  • A knowledge base you can query ("What's the difference between epoll and kqueue?")
  • A fact-checker for your understanding, not the other way around ("Is this how Rust's borrow checker works?")
  • A rubber duck for reasoning through problems (maybe, I'm split on this one.)
  • A documentation pointer ("Where do I find info about Zig's allocators?")

What AI should NOT do:

  • Write code
  • Debug your application - it's yours, debug it yourself
  • Explain documentation too much. Being able to read documentation is one of the most valuable skills for an engineer. Maybe just second to being able to read code.
  • Prevent you from suffering and struggling when something is not working

Maybe controversial, but the suffering is kind of the point. When you're stuck on a compiler error for an hour, reading documentation, trying different approaches, that's when learning happens. And believe me, after that you will not forget what the issue was. AI that "fixes it for you" steals that opportunity.

I came to a point where I would like to not use AI at all for learning, but that is not great either. It's a tool, and the best way I found so far to use this hammer without every problem becoming a nail is by using AI as read-only. It doesn't write my code, debug my programs, or solve my problems. It's just a fact-checker/knowledge-base dumb machine. It can't access my code, and cannot tell me what to do.

Finding the right project

When you're learning something new, project selection matters more than you think.

Don't try to invent something. You (probably) won't. And even if you do, you'll spend more time on product features than on learning. You will spend weeks on UI, auth, webhooks, and never get to the deep programming concepts you wanted to learn. Who cares about the UI if your goal is to learn how something works under the hood. I get the part where you might need to feel like you are making something that someone else could use to justify the effort. But this is the wrong goal if you want to learn. Learning is the goal, not people using your app (which they probably won't).

Don't try to fill a gap either. This leads to massive feature creep. "Oh, but this other tool doesn't have X" - now you're building features, not learning fundamentals.

Rebuild something that already exists.

This is counterintuitive but powerful. Build a text editor. A terminal emulator. A key-value store. Redis already exists, so does Vim, so does every terminal. But you haven't solved them. Anything that you do will be worse than the ones that exist, but that doesn't matter. It actually takes away a lot of the distractions of building something, like thinking about what it should do. If you are implementing your own terminal, you know exactly what it should do.

For one of my projects, I chose an encrypted in-memory key-value store (both in Rust and Zig, more on that later). I get to learn:

  • Memory management (how do hash maps actually work?)
  • Network & I/O multiplexing (epoll/kqueue, event loops)
  • Wire protocols (how do clients and servers talk and serialize the data)
  • Encryption (how do you actually encrypt data at rest, can I have a true fully encrypted data store without creating client-side encryption and an obfuscated KV store?)
  • Systems programming concepts without the feature noise

The project has a clear scope: GET, SET, DELETE, EXISTS. Four commands. No lists, no pub/sub, no Lua scripting. Just enough to learn the fundamentals.

If you're stuck for ideas, check out CodeCrafters, they have good project-based learning projects (I haven't used their service, but the project ideas are solid).

The value of exploration

Learning a new language "just because" is underrated. You don't have to be that guy who always wants to start a project in the most obscure, new, non-adopted, beta language that doesn't fit your use-case at all.

(You can be like Tom and invent one if you want)

You might never use Zig professionally or even use it again after this project. But you'll take something with you.

Different languages encode different philosophies:

  • Zig says: "Keep it simple, give the programmer full control, trust them to manage memory correctly (which you won't)"
  • Rust says: "F**k you, you know nothing. I'm better than you (which is true)"
  • Javascript says: "i NeED a sUePErSet tO Be KIndA usEFul"

They're different approaches to the same problem, learning them makes your brain more malleable. You stop thinking in one paradigm and start seeing tradeoffs. When you encounter a problem, you have more tools in your mental toolkit. "Oh, this is like Zig's defer pattern" or "This would be cleaner with Rust's Result type."

Even if you never touch these languages again, you'll carry these patterns forward. You'll understand why memory safety matters. You'll recognize the cost of abstraction, and its power when done right. When manual control is needed and worth the hassle and the risk.

Exploration is one of the ways to form somewhat educated opinions about different ways to solve problems.

The parallel learning approach

There is a problem with language comparisons: ideally you build something in Language A, then rebuild it in Language B. But the second time, you already know the solution. You implement it better, faster, with fewer mistakes. The comparison is unfair.

I wanted to compare Zig and Rust fairly. So I'm building the same encrypted key-value store in both languages, module by module, in parallel. First I built my own hash map implementation in Zig, then I did Rust. After both were done, I moved to the wire protocol, serialization and deserialization, first in Rust and then in Zig. I'm currently powering through my own event loop for I/O multiplexing in Zig. Which will probably be orders of magnitude worse than any library, but again, that is not the point.

This approach is painful, there is a lot of context switching, and you are pushing maybe too much information into your brain. Slow down, don't try to finish the project quickly, do the actual thing you were trying to do, which is a fair 1:1 comparison.

What I've learned so far (2 modules in):

Module 1 in Zig: The language got out of my way, but I spent hours learning about memory. How does Zig HashMap work internally? (It stores pointers, not data). What is a use-after-free vulnerability, how do you test for memory leaks.

Module 1 in Rust: The opposite. I didn't think about memory management at all. Instead, I focused on the type system. String vs &str, why do I need .clone() everywhere?

Module 2 in Zig: Discovered a subtle memory leak pattern in my serializer. Some responses returned string literals ("OK"), others used allocPrint() which allocates. The caller didn't know which responses needed freeing. In Zig, you discover and document these patterns yourself.

Module 2 in Rust: Enums with data are amazing. The type system made the parser clean and safe.

Both teach systems programming, just different aspects. Zig forces you to understand memory management deeply. Rust forces you to understand ownership and borrowing deeply. Same problem, different enforcement. Doing them in parallel prevents me from saying "Language A is better" without understanding why each makes the tradeoffs it does.

Before I started this project, I was bullish about Zig and scared of Rust. So far I have not encountered any complexity in Rust (no async stuff yet), borrow and ownership was actually pretty easy after learning a lot about memory management while preparing for Zig. Up until now, I'm more bullish on the Rust side than Zig (I still find Zig amazing, and so far there is space for both in my heart).

Project logs and knowledge bases

This is the core of the framework, inspired by the Zettelkasten method (see Sönke Ahrens "How to Take Smart Notes" if you want the philosophy behind this).

I maintain two files for each project:

knowledge-base.md: Concepts explained in my own words. Not copy-pasted from documentation. If I can't explain it simply, I don't understand it.

Example entry:

### Zig HashMap memory management

What HashMap allocates:
  - Internal structure: buckets, metadata, hash table arrays
  - Uses the allocator you pass at init()
  - map.deinit() frees only this internal structure

What HashMap does NOT allocate:
  - The actual key/value data you pass to put()
  - It stores slices (pointer + length) pointing to YOUR memory
  - Does NOT copy the string data

Memory ownership problem:
  - HashMap stores pointers to wherever you allocated keys/values
  - If that memory gets freed (e.g., arena deinit), hashmap has dangling pointers
  - Use-after-free = undefined behavior

This is a critical insight I would've missed if I'd just asked AI "how does HashMap work?"

project-log.md: raw documentation of the learning process.

Example entry:

### 2025-11-14 - Zig's Real Difficulty: Understanding Memory, Not the Language

After implementing the Storage module, I realized Zig itself is not hard. Systems programming is hard.

The language syntax is simple and clean. But using it requires deeply understanding:
- How memory works (stack vs heap)
- What pointers actually are vs string literals
- How HashMap works internally (it doesn't copy data, just stores pointers!)
- Manual lifetime tracking (all in your head)
- Use-after-free, double-free, dangling pointer

A bit more to convince you

  1. Writing is learning. Explaining a concept in your own words forces you to understand it deeply.
  2. Future reference. When I forget how HashMap works, I don't re-google. I read my own explanation.
  3. Blog material. These raw logs become blog posts later, but with the authentic struggles preserved.
  4. Pattern recognition. Looking back, I can see where I struggled and where things clicked.

The Zettelkasten method emphasizes that notes should be written as if teaching someone else. Not "I learned X today" but "Here's how X works and why it matters." That shift in perspective forces clarity. I highly recommend that book. It is easy to read and it does not spoon-feed you how to do your own notes. It just tells you how some prolific academics do it, and why. It's up to you what you take away from it and what you ignore. (Contrary to 98% of all other productivity or self-improvement books that sell you a magic recipe)

Where I am now

I'm 2 modules into a 7-module project. The framework is working.

My knowledge base has grown organically with entries on what is actually a process, IPC, memory management, wire protocols, encryption concepts, concurrency models, different async strategies, and Zig/Rust-specific patterns. I could already start writing blog posts about what I've learned. But I don't have to, whenever I feel like it, all my notes will be there (the blogs write themselves basically, you just create a thread to put all the different notes together)


The framework isn't about completing projects. It's about maximizing learning per unit of time.

If you're learning something new, try this:

  1. Pick a project to rebuild (not invent)
  2. Create knowledge-base.md and project-log.md
  3. Write in your own words
  4. Use AI as a read-only resource or not at all.
  5. Embrace the suffering

The code you write matters less than the understanding you gain. Build things. Break things. Document the journey.

Table of Contents