Batuhan Saka
Trendyol Tech
Published in
8 min readMar 6, 2023

--

Revamping Trendyol’s iOS App: A Modularization Success Story

Photo by Coline Beulin on Unsplash

If you’re not familiar with Trendyol, it’s one of Turkey’s leading e-commerce platforms, offering a wide range of products from fashion to home decor. But Trendyol is more than just an online retailer — it’s a super-app that brings together several omnichannel services, including meal ordering, grocery ordering, international shopping across Europe, and second-hand marketplaces.

Trendyol Super App (Turkey core commerce, International core commerce, Second-hand, Meal, Grocery)

Behind the scenes, our talented team of developers is working hard to keep the platform running smoothly and efficiently. In recent years, we have embarked on an ambitious journey towards modularization, with the goal of improving developer experience, scalability, and build times. In this article, we’ll dive into the progress we have made, and the lessons we’ve learned along the way.

At the start of 2020, we were far from a monolithic iOS application. With over 170 modules in our codebase, we struggled to navigate the challenges of circular dependencies, dependency hell, and build settings management. By fueling the problems we tried to avoid with the solutions we made, we caused it to grow. Despite our best efforts, these issues slowed down our development process and caused frustration for our team. However, we were determined to find a better way forward.

You can see the changes we have made in the Trendyol app over the years in the timeline below.

Trendyol iOS app modularization timeline

Even back then, we were aware that in order to align with the direction our application was taking, we needed to move away from a monolithic structure and towards a more modular approach. However, the rapid growth of the project, combined with certain practices we had adopted and the business needs we were trying to meet, brought forth a set of distinct challenges.

Let’s talk about the improvements we have made in sections below and the steps we have taken.

Attempt to migrate from CocoaPods to SPM

Until 2021, we were using CocoaPods as our package manager to include third-party packages in our project. CocoaPods directly integrated with our Xcode project and brought along its own targets and schemas. It also stored the downloaded third-party frameworks’ resources in a folder called “Pods” in the project directory.

We were sending the Pods folder to the remote repository, which was significantly increasing the repository size and negatively affecting the Git history. Additionally, since CocoaPods was a black box, we sometimes had to spend hours to resolve frustrating build issues.

So we decided to try Apple’s newly announced SPM and integrated it as a POC. We successfully replaced all the third-party packages from CocoaPods to SPM. However, when we tried to include our internal feature and helper modules with SPM, we faced resolving issues that significantly affected the developer experience. We saw that there were a few discussions about this issue on Apple’s forums as well. Therefore, we had to give up on this option.

Xcode at scale

One of the main issues was the difficulty in managing a growing number of .xcodeproj files, which in turn led to a decrease in consistency between build configurations.

For example, we noticed that due to manually setting build configurations for all Xcode projects and not using a tool to keep them in sync, we had left the optimization level at -Onone in a few projects. This resulted in increased application size. Learn more.

Additionally, conflicts between .xcodeproj and .xcworkspace files can be frustrating and error-prone when working on a monorepo with a large team.

As a result of all these issues, Tuist became our savior. In the following section, you can see how we benefited from it and how it helped us improve our processes.

Tuist migration

Tuist automates the generation of your Xcode projects and workspaces with the specified configurations, ensuring maximum consistency. It simplifies managing configurations across projects and helps you avoid drowning in the hundreds of interfaces available in Xcode. Learn more.

In fact, you can even create templates specific to your project and create a new module in less than a second with a single CLI command. Learn more.

What have we started doing after Tuist migration?

  • Eliminated the need for committing Xcode project and workspace files to version control by generating them at runtime, reducing the risk of merge conflicts and enabling greater flexibility in managing project files
  • Ensured consistency across projects by eliminating manual configuration of Xcode projects
  • Reduced the complexity of managing Xcode projects and workspaces by simplifying configuration with Tuist
  • Improved productivity by reducing the time required for setting up new modules with the help of Tuist templates
  • Improved the overall developer experience by reducing the number of manual steps required for project configuration and setup.
  • Utilizing Tuist’s graph command to visualize and analyze the dependency graph of our project, allowing us to better understand the relationships between modules and identify potential issues or optimizations.

Third Party Dependencies as XCFrameworks

Before Tuist migration, we were using CocoaPods and linking 170+ internal frameworks in our application dynamically. This resulted in compiling third-party code repeatedly, leading to a larger application size and longer pre-main time.

After migrating to Tuist, we converted these frameworks into XCFrameworks and linked them to our application statically. Additionally, we serve these third-party XCFrameworks ourselves, version them, and include them in the project with a central SPM project.

This allowed us to include them at compile time, avoiding these issues and resulting in faster compilation, smaller application size, and less pre-main time.

Static vs Dynamic Linking comparison

As you can see in the table, using dynamic frameworks increases your pre-main time and also negatively affects your application size. However, it should be noted that static frameworks have some drawbacks along with their benefits. You need to do bundle matching and resource management yourself.

Using Interface Modules

Partitioning your application into modules may enhance the build times. However, in the absence of comprehensive understanding regarding the organization of your dependency graph, even a single line of code change can significantly impact your incremental build times, leading to a poorer developer experience.

If you were developing a small application, you would likely build a monolithic structure and keep all code and resources under the AppTarget. This is not a problem as long as the scale of your application is small. However, in larger applications like Trendyol with hundreds of modules, even the smallest change you make can cause the entire app to be recompiled.

Monolith application (code and resources bundled into same target)

Assuming you have decided to modularize your application and separated its screens/features into their own modules, you would end up with a structure similar to the following.

Modular application (each feature modules’ code and resources bundled itself)

At first glance, everything may seem fine, but as the number of screens increases, these feature modules will need to add other modules as dependencies, and we will end up with a dependency graph like the one below.

Modular application (Concrete modules adds each other as dependencies)

Since concrete modules have added each other as dependencies, we will face the same problem here as we did in the monolithic application. Because any change made to the those modules will cause all modules to be recompiled.

Therefore, we decided to use Interface Modules. In this approach, each module has its own Interface target. As far as I know, Spotify and some companies prefer to create an extra Xcode project to store Interfaces in the target of that project, instead of adding Interface targets to concrete modules. However, we chose this method because we want to keep the number of modules down and we believe that it is more organized. In this target, we only use primitive types and models necessary to initialize the modules, and we make sure not to add other modules as dependencies.

Public API & Impl separation

By having an initialization class and an interface for each module, we are able to keep the access modifiers of the classes that can be accessed from outside the module as internal. This means that only the classes that we explicitly mark as public in the interface are visible outside the module. This allows us to better control the module’s public API and ensure that the implementation details remain hidden from external modules, providing better encapsulation and reducing the risk of unintended coupling between modules.

Once you have separated your modules into concrete and interface targets, you need to register their implementations and public APIs at runtime through your dependency container structure.

Afterwards, during runtime, you should access and use the modules through interfaces via your dependency container structure.

Modular application (separated Public API and Impl targets)

As seen, no concrete module is now dependent on another concrete module. They communicate with each other directly through the Public API. Thus, we ensure that only the places we make changes to are compiled.

It should be noted that as concrete modules become more dependent on each other, Xcode’s build process becomes more bottlenecked. This is because with vertical dependency graphs, dependencies are built one by one and we cannot take advantage of parallel building. Therefore, if we pay attention to creating horizontal dependency graphs, we can inevitably achieve better build times.

When we started this journey, we did not have a dedicated team at Trendyol that specifically focused on these issues. In response to these needs, we established a Platform team and began conducting research and work with a Developer Experience focus. In order to provide a basis for comparison, I would like to share a few comparisons related to our build times, pre-main time, and application size.

Trendyol iOS App Performance Metrics comparison

Please note that the old metrics provided several years ago were based on an earlier stage of our improvements, and since then, numerous features have been added and multiple frameworks have been integrated into Trendyol iOS application.

Additionally, we can say that the size of Trendyol’s iOS team working on the application has grown fivefold in the last 2 years.

TL;DR

Trendyol migrated its iOS project to Tuist to improve developer experience and build efficiency, resulting in reduced build times, smaller app size, and better dependency management. The creation of a dedicated iOS Platform team enabled focused research and development, leading to significant improvements in the platform’s metrics.

More Info

If you’re interested in improving modularization and performance metrics, you can check out a few resources we benefited from:

Want to work in this team?

Do you want to join us on the journey of building the e-commerce platform that has the most positive impact?

Have a look at the roles we’re looking for!

--

--