Post

Navigating Multiple Technical Migrations in Our Domain - Motivation and Strategy

Click here to read Medium version.

In complex software projects, the pursuit of feature development and the need for refactoring to reduce technical debt often resembles a zero-sum game, posing a challenge in harmonizing these crucial aspects. In the Trendyol Android Project context, we aim to seamlessly integrate them and answer a straightforward question: Can we simultaneously enhance user and developer experiences?

This article marks the beginning of a series where we’ll delve into our experiences, challenges, and progress in migrations and refactors. In this post, I’ll detail the reasons behind these undertakings, define the tasks, and outline our strategy.

An overview of our working methodology

To help you understand our technical decision process and approach, talking about our working methodology in Trendyol is an excellent place to start.

We have several domains in the Trendyol Android Project, each offering distinct functionality within the app. Our team, known as the Product Detail Page (PDP) team, is responsible for the user experience that starts with displaying the Product you are looking for and ends with adding the product to your Cart. Each domain has a unique set of modules they oversee, along with shared modules accessible to all domains. This grants each domain team a degree of autonomy in choosing technologies and libraries for their specific areas.

In our development process, whenever a technical change is needed, the responsible domain or team member prepares an RFC (Request for Comments) and shares it with the team. Depending on the complexity of the change, there may be further discussions and revisions to refine the proposed solution. After the review period concludes, the solution is either accepted or rejected. This iterative approach enables us to receive early feedback from our colleagues and identify the most optimal solution for the given problem.

Motivation for Change

The Product Detail Page screen features various components tailored to the displayed product type. These components are highly reactive, and interactions with one component can trigger multiple updates on others within the screen. We follow the MVVM pattern in our presentation layer to structure the necessary logic. Our ViewModel is injected with classes that further encapsulate the required business logic, aligning with clean architecture principles.

User Navigation inside PDP Domain

In its early stages, the Product Detail Page Screen contained several distinct components presenting relevant product information. Key components included a product image slider, seller and product info areas, sliders for user reviews and questions, displays of similar products, and a feedback collection area. Some components also featured horizontal sliders. To accommodate these elements, the screen was built using a NestedScrollView.

Some of the Components in Example PDP Screen

This approach offered numerous advantages, the most significant being the ease of adding new components and testing various variations. We implemented components tailored to specific product categories or display conditions, allowing us to explore and present meaningful information about the product continuously. Our iteration speed has greatly benefited from this design.

However, the increased number of components highlighted a critical issue affecting our flexibility. Currently, we have 43 distinct components on the Product Detail Page Screen, and we expect this number to reach 50 once the ongoing feature developments and refactors are completed. The potential number of components we could display in any product session surpassed those shown. And because we use NestedScrollView, every component we might display is present in the Layout when the screen is opened.

We also encountered challenges in modifying and maintaining certain parts of the codebase. Some screen components became difficult to test, and specific business logic became tightly coupled with the Android Framework. For example, our Hero Button visibility is tied to 6 different API calls and four different component layout visibility. When we test different variations in the layout, this logic should be self-contained and tested for potential edge cases.

Another notable issue arises from breaches in the MVVM pattern within the ProductDetailFragment. According to the standard MVVM pattern, data in the ViewModel should be created or updated based on user interactions or a separate Business Flow. Subsequently, this processed data is exposed through an observable data stream for presentation on the screen. However, in our case, a significant portion of this data is also accessed directly within the Fragment to handle various operations such as analytics and navigation. Let’s consider this showVariantSelectionDialog code for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override fun showVariantSelectionDialog(pageType: String) {
	val product = productDetailViewModel.getProduct()
	if (product == null) {
		showProductDetailErrorDialog()
		return
	}
	variantSelectionDialogProvider.provideVariantSelectionDialog(
		content = getVariantSelectionContent(product, pageType),
		anyVariantSelectedListener = {
			productDetailViewModel.onVariantSelected(it)
		},
		addToBasketClickedListener = {
			productDetailViewModel.onVariantSelectionDialogAddToBasketButtonClicked(it)
		},
	).show(childFragmentManager, VariantSelectionDialogProvider.TAG)
}

This type of configuration code introduces two main problems. Firstly, it becomes more challenging to test, as the configuration logic for displaying the dialog is now embedded in the Fragment. Secondly, over time, there is a risk that developers may inadvertently extend this logic to encompass additional business logic, leading to increased code complexity.

Overall, these couplings and constraints would complicate future improvements and refactors, such as changes to how we display components on the PDP Screen or transitioning to Jetpack Compose. With several upcoming project-wide technical migrations on the horizon, we decided to address these issues comprehensively.

Starting Point

We began by identifying pain points in our overall architecture and determining areas needing improvement. To summarise our design before the changes:

  • We adopt the MVVM pattern across all our screens.
  • Dagger is the chosen dependency injection framework for our modules.
  • RxJava is crucial in handling reactive computation in both UI and data layers.
  • While MVVM is our pattern of choice, we acknowledge direct data access to the ViewModel for certain navigation logic and analytics operations, which can compromise the desired decoupling between View and ViewModel.
  • Some business logic relies on this direct data access, making composing logic within the ViewModel challenging.
  • Testing becomes more challenging due to the logic residing in Fragment classes.

Plan of Attack

Our objective with this overhaul is to address design shortcomings and modernize our technology stack in the PDP domain. Here’s the list of migrations and refactoring tasks planned for PDP 2.0:

  • Transitioning the DI infrastructure to Hilt.
  • Shifting from RxJava to Coroutines.
  • Refactoring business logic to rectify MVVM violations.
  • Adapting the PDP Screen layout to use a RecyclerView-based listing.
  • Modularizing sections of the PDP domain for better decoupling.
  • Migrating components to Jetpack Compose.

Our primary concern with these multiple technical migrations is preserving our regular feature development momentum. To achieve this, it’s essential that these migrations and refactors should be carried out in parallel and incrementally. This approach is vital to prevent any implementation blockers from disrupting potential feature releases and impacting the release train concept we adhere to.

Monitoring six different migration and refactoring tasks could strain our workload and make progress tracking challenging. Therefore, we’ve prioritized the first three tasks in the list to pave the way for the remaining three.

Conclusion

Our primary goal with these migrations and refactor tasks is to enhance the maintainability of the PDP domain and ensure our architecture remains adaptable for future challenges. Upon completing the initial three tasks:

  • We will align with the Trendyol Android Project’s selected DI and concurrent programming methods.
  • Our presentation layer architecture will become more maintainable and testable.
  • The MVVM pattern will be correctly implemented within our domain. Entry points for data updates and observable effects will become deterministic and the sole means of updating the screen’s state.

Next in the Series

In the next post, we will discuss how we prioritized these migrations and delve into the business logic refactoring. Thank you for reading, and we look forward to seeing you in the next post!

This post is licensed under CC BY 4.0 by the author.