From Intern to Principal Product Engineer: how side projects supercharged my career

I’ve been passionate about technology my whole life. I started with Turbo Pascal as a kid and have since worked with embedded systems, front-end/back-end development, and machine learning projects.

iOS/macOS came into the picture during a short university course. Within a month, I had learned a new language and landed an internship at Readdle. Five years later, I became a Principal Engineer. Still, I'm mentoring new teammates, participating in interviews, and developing leadership skills to keep growing.

My journey from having zero experience with Apple platforms to reaching my current role inspired me to reflect on my learning approach and outline the key directions that helped me grow. I believe my experience can be valuable to other engineers who feel stuck or unsure about their next steps.

Here are the key approaches and resources that helped me, and might help you too.

How side projects accelerated my growth

After reaching the middle level, I hit a professional plateau. I felt like I was completing tasks quickly and efficiently, fixing bugs with no issues, and having a solid understanding of designing architecture for new features. But I couldn’t quite grasp what separated me from being a true Senior Engineer.

The shift happened gradually. Looking back, I realized the difference wasn’t a big project at work or a promotion. It was all the time I had spent coding outside of work.

On evenings and weekends, I built small projects, sometimes just for fun, sometimes to test an idea. These side projects exposed me to real-world problems outside my usual scope. That hands-on experience directly translated into my work and gave me an edge when tackling unfamiliar issues.

The biggest lesson was that most skills in software development aren’t tied to a single language or platform. A few examples:

  • Even if you’re reading about iOS/macOS app architecture, you’ll likely recognize similar patterns in web development or embedded systems.
  • Knowledge of Python in the context of machine learning can help you quickly write the necessary script to automate CI processes in a completely different domain.
  • Operating system basics will help you understand entire classes of problems across any programming language. After all, any program, regardless of the language, ultimately interacts with the operating system similarly.
  • One of the first books I read was Effective Java—a language I haven’t used in years. But that book taught me object-oriented principles that apply across the board. That’s the kind of knowledge that sticks.
  • If you enjoy programming, these side projects won’t feel like work. In my experience, the best engineers are those who code because they love building and learning.

Ways to learn anything

Some people seem to learn faster than others. It can be a new programming language, a pattern, an architecture, or even something entirely unrelated to development, like learning to play chess. I believe that’s because learning itself is a skill—a meta-skill you can practice.

Reflecting on my own learning journey, I noticed recurring patterns in approaching new topics. I think this stems from my time at school and university, where juggling multiple disciplines pushed my brain to optimize similar patterns. This allowed me to learn faster.

Now, I have a specific process for learning something new. I don’t think this process is groundbreaking, but it has allowed me to stay focused and intentional in both learning and self-growth. In this section, I’ll walk you through the three stages of my learning approach: Screening, Structuring, and Practice.

Screening

A good source of information is a crucial first step when learning a new skill. Here are a few recommendations based on my experience:

  • Most information on any topic is available for free. There’s rarely a need to pay for courses or lectures unless you are absolutely sure the content is unique. But if you are starting out, you won’t yet have the expertise to tell the difference between genuine insights and low-quality material.
  • Text-based resources are usually more concise and easier to digest than videos, especially if you’re a fast reader. Over time, I learned to quickly skim and filter out irrelevant information, something that’s harder with video content.
  • Over time, I developed the ability to skim and filter irrelevant information, which isn’t possible with video materials because it’s linear and time-consuming. It’s also easier to bookmark or copy a fragment in the text than to look for the right moment in the video.
  • English resources are much easier to find than resources in other languages, especially when it comes to programming and technology. Therefore, being able to read and understand English quickly is a must!
  • Don’t skip the classics. Books recommended repeatedly (like Clean Code or Design Patterns) are often still the best material available.
  • I also recommend building your own personal library of books across different topics. That way, you’ll always have reliable material at hand. Personally, I keep a simple folder on my computer.

Structuring

Once I’ve gathered a few solid sources, I structure the information. This process includes:

  1. Processing the sources. While reading a book or article, I highlight key points with bookmarks or copy them into my notes for future reference.
  2. Cross-referencing information. Especially when the topic is subjective, it is very important to compare several solutions and perspectives. Information that coincides in several different sources is likely to be true. When sources conflict, I usually postpone deciding until I’ve gained more personal experience with the topic. Later, with deeper understanding, I can choose the best approach.
  3. Centralizing knowledge. To analyze all the information received and found, it needs to be stored in one place. I use Obsidian to centralize my knowledge. It’s free, and I find the markdown format very convenient.
  4. Storing. I usually save articles and useful links in simple markdown lists. This helps me quickly find a specific resource without digging through browser history. Plus, if I return to the topic in the future, I already have a good foundation saved.

Practice

Reading isn’t enough. You only really learn when you build something. I try to reinforce every topic I study by creating small projects. When it comes to programming, this usually means creating a simple application that demonstrates my ability to apply the necessary skills. Quick practice also immediately reveals any gaps in understanding. I often thought I understood everything I needed to, only to run into unexpected problems just 10 minutes into implementation.

One example that stands out is learning UI architectures. When I first studied MVVM in the context of iOS development, I thought the architecture was simple and straightforward. But when I tried building a basic to-do list app, I immediately ran into a knowledge gap. The resources explained how View communicates with ViewModel, and how ViewModel interacts with Model, but none of them addressed who should create what and how!

My application had a table displaying the current to-do tasks. Should I create a separate ViewModel for each task? Or a single ViewModel for the entire table? And what if I needed both ViewModels? Who creates which one? These questions opened the door to understanding how to coordinate components in similar architectures.

Later, while working on PDF Expert, our team decided to try implementing MVVM. I was able to build the architecture and help lead the team through the development of that feature. That’s the kind of experience I wouldn’t have gained if I hadn’t first built that small, messy to-do app on my own.

My recommendations for side projects

  • Find inspiration and an idea. The idea usually comes first, then I tie it to a specific learning goal. The idea doesn’t have to be unique or even particularly useful, because the main purpose is to gain experience. For example, I might decide to build a habit tracker app. From there, I can choose to focus on architecture or use this idea to design an impressive UI with animations. Or maybe I need to build a server for this app? That could be a great opportunity to try out new backend technologies.
  • Define “done.” If you try to bring every project to production, you must allocate a lot of time for each. Therefore, I believe you need to define the milestones you want to achieve with each project.
  • Perfect is the enemy of good. If you get too caught up trying to find the “perfect” solution, you may lose motivation entirely.
  • Minimize unnecessary tasks. Does the exact color palette matter in an app meant to teach you about architecture? Or the logging system? The most valuable projects were the ones focused on a single learning goal. The less time I spend on low-priority details, the more I can invest in what truly matters.

10 topics that changed how I build software

Here are 10 areas I explored on my own that impacted my career.

UI application architectures

Any engineer who’s worked on UI-based projects is familiar with the classic code organization patterns. Who hasn’t heard of MVC, MVP, MVVM? In my experience, simple theoretical knowledge can give you confidence to start a new project, but without practical experience, it often falls apart at the first sign of complexity.

I once thought I fully understood how MVVM works, but I didn’t have the experience to try it. I realized this when I tried to develop a small MVVM application. It became clear I needed coordinators and routers to navigate and assemble the mishmash of classes into a coherent structure. That experience taught me a key lesson: there’s no one-size-fits-all architecture. An architecture that works well in one project (or even one screen) might be a poor fit elsewhere.

Design patterns

Anyone who’s been through developer interviews has likely encountered questions about design patterns. Many engineers seem to recognize certain patterns in theory but struggle to apply them in practice. The simplest way to bridge that gap is to build a small project focused on implementing a specific pattern.

The original book often uses a text editor as an example. In my experience, editors of different documents require the use of many design patterns. It can be a text editor, an image editor, or even a diagramming program.

Testing

Code testing is a cornerstone of stable codebases. And although the idea seems simple – “just write tests!” – writing useful tests is often far from trivial.

The main challenge is that maintaining tests consumes team resources. If you have to rewrite tests after every code refactoring, the overall value of those tests becomes questionable. This concept is known as Test Fragility, and in my experience, it is not as well-known as it should be. You can learn about the testing theory, for example, in this book.

A strong focus on testing also affects code quality. In general, code written with testing in mind will be more modular and expose a cleaner, simpler API. It’s easy to try testing in practice – just integrate tests into an existing project or start a new one with a focus on writing tests.

Algorithms and data structures

Knowing algorithms isn’t a strict requirement in modern software development. Many companies still include algorithmic challenges in their interviews, even though those skills may not be needed in day-to-day work. Still, I believe it’s worth investing time in learning algorithms because it is fun and interesting. Plus, today you can explore these topics interactively through platforms like LeetCode.

If you ever get the chance to participate in algorithmic programming contests like ACM ICPC, I highly recommend giving it a shot. Even if this knowledge doesn’t seem directly useful at first, code optimization techniques can be applied to any project, and taking part in a team-based competition is an experience you’ll likely remember fondly.

Multithreading

Multithreading is a fascinating topic. You can spend years writing code without understanding even the basic concepts. Every developer knows that blocking the main thread will freeze the app. But if something goes wrong, only developers who truly understand multithreaded code and can visualize the full timeline of events can diagnose those elusive, phantom bugs.

In my opinion, understanding synchronization primitives, threads, and the consequences of neglecting these concepts is one of the key factors that separates a mid-level developer from a Senior one. To understand the basic concepts, I recommend the free book The Little Book of Semaphores.

For hands-on experience, you can try writing a program that performs heavy background processing and synchronizes the results with the UI, especially if the background work itself involves multiple threads. Simulating physical processes can be a great starting point for this kind of project.

Graphics programming

Graphics processors are one of the few parts of a computer that most developers rarely have to interact with directly. For the majority of applications, UI rendering either happens on the CPU or is abstracted away from the actual GPU device, so you don’t even need to think about it. But once the number of on-screen elements grows large enough, and high-level abstractions can’t keep up with the required frame rates, you need to dig deeper. Understanding the problems GPUs are designed to solve, as well as the ability to “explain” to them what needs to be rendered, can help you quickly identify solutions in similar situations.

A classic project for learning graphics programming is building a game engine. If you love computer games like I do, you’ve probably wondered how they’re programmed. There are tons of resources online for learning Metal, DirectX, OpenGL, or Vulkan. Just don’t expect your project to become the next Unreal Engine. But if you can reach the point where you’ve built a small game running on your own engine, I guarantee you’ll encounter fascinating challenges and gain valuable knowledge.

Embedded programming

When I first learned that even the smallest electronic devices around us run their own programs and processors, I was immediately fascinated and wanted to try building something similar. However, developing for microprocessors comes with many limitations. Such systems usually have less than 128 kilobytes of memory, and dynamic memory allocation is usually off the table.

Writing and debugging embedded code presents many challenges. But, as a result, you can see your code come to life in the real world. Even getting a simple LED to blink felt like a breakthrough for me. Understanding how microcontrollers work is a significant first step toward grasping how modern CPUs and operating systems work. When working with Embedded devices, you may even need to dip into platform-specific assembly language (typically ARM).

Today, it’s easier than ever to get started with embedded development. Budget-friendly microcontrollers like the STM32 line are a great place to begin, and many ready-to-use boards come with built-in programmers that connect via USB. The possibilities are endless, from dynamic room lighting to building your own smart home IoT hub.

Disassembler

Sometimes, being able to look one level deeper into your code can be invaluable. For example, in Apple platform development, we often work with mostly closed-source libraries from Apple. When something doesn’t behave as expected, it’s usually necessary to file a ticket through Feedback Assistant and wait (sometimes for years!) for a response. But with the help of a disassembler, you can often peek under the hood and understand how the closed-source code actually works.

Understanding your own program's assembly code can also be useful, especially in the context of optimization. Only by inspecting the compiled code can you truly know what your code is doing. Personally, I use Hopper Disassembler on macOS for this purpose.

Compilers

The programming world has its own “secret clubs.” If we think about the people whose products impact the most engineers, we inevitably arrive at those who build the compilers behind our favorite programming languages. Until I spent time studying compilers myself, these programs felt like pure magic. But even learning the basics, the magic fog still doesn’t allow you to fully grasp the brilliance of those who’ve created entire programming languages.

The good news is that plenty of resources and books are available on the topic. Two of my favorites are Engineering a Compiler and, of course, the classic Dragon Book. You can dive into the development of your own programming language by starting with the Kaleidoscope tutorial by LLVM. In “real-world” software development, unless you’re a compiler engineer, this knowledge can give you a much deeper understanding of language behavior and obscure errors. Plus, most compilers are open-source, which allows you to contribute to massive, high-impact projects. For instance, contributing to the Swift repository substantially adds to any developer’s portfolio.

Operating system fundamentals

If there are projects even more complex than compilers, operating systems come to mind. As developers who spend most of our time working on computers, it’s surprisingly easy to forget that an operating system is just another program, like the ones we write ourselves. A massive layer of abstractions from the hardware creates the illusion that every user-space program runs on its own processor with its own memory. These abstractions are so accurate that the code written and compiled once can reliably run across a huge variety of hardware configurations.

The book Operating Systems: Three Easy Pieces is a great starting point for understanding how operating systems work. And if you’re specifically interested in iOS or macOS internals, I recommend the free resource MacOS X and iOS Internals. Gaining insight into how an OS functions bridges the final gap between hardware and the code we write. For a developer, this means almost complete control and understanding of what happens when your program runs.

To wrap it up

Looking back, I didn’t follow a strict plan. I just kept learning, building, and chasing what interested me. That curiosity turned into a career.

If you’re comfortable but wondering what’s next, try going wide. Explore something new. The time you invest in side projects and learning will compound, and the gaps between levels—Junior, Middle, Senior, and Principal—will start to shrink faster than you think.

There’s always more to learn. But with each project, each mistake, and each new concept, you’re moving forward. Keep building.

— Andrii Zinoviev, Principal Product Engineer


Want to become a part of our Engineering team and impact millions? Check out our vacancies


Get news and recommendations

Stay up to date with our news and announcements by subscribing to our Newsletter.

By clicking on "Subscribe" I agree to the Privacy Notice.