There and Back Again: The journey of the GO-LIFE app

Lessons in refactoring from a team that learned things the hard way.

There and Back Again: The journey of the GO-LIFE app

By Raditya Gumay

In November 2016, we started working on the GO-LIFE app. Since then, many changes were made to the architecture, UI, and flow of the app. Two years later, we’re ready to share our experience and impart what we learned in the process.

For the uninitiated, GO-LIFE is GOJEK’s on-demand lifestyle app. We offer massages, cleaning, automotive, mechanic and beauty services to more than 1 million users across 16 cities in Indonesia. Here’s what the app looks like today:

Figure 1: The GO-LIFE app

This blog post is about some of the mistakes we made building the app and what we learnt. The big takeaway for us:

Creating software is not just about writing code. It’s also about the people. Having a good team improves the entire ecosystem.
Architecture is just one metaphor for software development, and in particular for that part of software that delivers the initial product in the same sense that an architect delivers a pristine building ~ Robert C. Martin
Figure 2. The Code Measurement

I bet you guys are familiar with Robert C Martin and his teachings.

In fact, code quality is not the only measurement in the above formula. We measure our codebase by using 3Rs (readability, readability, readability). During development, we all face many problems; how we deal with concurrency, transformation, sync between local and remote data, custom views, dynamic views, immutability, and more.

Every single problem has its own solution using proper design patterns. Before we go further, let me explain our development process a bit.

Figure 3. Development LifeCycle

Build — Measure — Learn
This is something every software engineer must keep in mind. We are not wizards, and application development has a cycle.

  1. Build: This stage of our development process includes product design sprint to a minimum viable product (MVP). This ends with a delivery prototype or A/B testing feature.
  2. Measure: As the name suggests, we measure feedback from users, what’s working and what didn’t. This is based on your initial hypothesis.
  3. Learn: Armed with user feedback, you should improve your hypothesis.
Always listen to your users to keep them engaged and avoid churn

Take a deep breath, we’re headed for the deep end.

Application Design

In the initial stages, what we can call GO-LIFE v.1, we only had four services, GO-MASSAGE, GO-AUTO, GO-GLAM, and GO-CLEAN. Currently, we have more than 8 services.

Early on, there were not many reasons for the user to spend more time using the app. The focus was all on the product; how to make the user do more and complete more orders.

Nowadays, we add videos, articles, vouchers and more features to entice users to dive deeper into our offerings.

Thanks to our researchers, designers, and PMs that are constantly working to improve and make our user experience better.
Figure 4: Transformation of UI

Application Architecture

In the development process, we made a lot of changes, which included both refactors and revamps. There are certain reasons as to why we do refactors on the design of the architecture. These are the main ones:

  1. To add features and fix bugs
  2. To improve the design
  3. Optimise resource usage
  4. Performance

The act of improving design without changing behaviour is called refactoring, To achieve this, we need good architecture (I will explain this later). This does not mean behaviour should never be changed, it depends on what the stakeholder needs.

The idea behind refactoring is we can make the application more maintainable, robust, extendable, and also testable.

The key thing about refactoring from a change point of view is that there aren’t supposed to be any functional changes when you refactor (although behaviour can change somewhat because the structural changes that you make can alter performance, for better or worse). ~ Michael Feathers

In our initial development, like any application that keeps growing, we started with a monolith that had only a single app module. At the time, we only had five engineers for Android (three for the consumer app, two for the merchant-facing Mitra app), and two for the iOS consumer app. Everything was fine, we delivered releases on time and no major crashes happened, PMs were happy, stakeholders were happy.

Sailing into the storm

Everything changed when we were asked to add more features. There was not enough time to add appropriate test cases, and the nightmares began. The target to release GO-LIFE v3 was mid-December 2017. But there was a problem.

We were not ready.

The timeline was tight, and unavoidable changes in the middle of the sprint mixed with less-than-ideal code architecture. The backend was completed just two weeks before release, our code coverage was not good enough, and we did not handle fragments appropriately.

We ended up needing six patches for GO-LIFE version 3.

Changing things in the middle of a sprint was not ideal, but we engineers made mistakes too. We did not satisfy all the use cases we had. For instance, in each API call we have two closures, a success and an error. Error exceptions have multiple types, but we did not have appropriate handlers for each one. This resulted in specific error types going to default.

Take a look at the code below (This is a current implementation detail, but the idea is the same as our initial development).

Figure 5. Error Closure

In the implementation detail of networkError.getResponse, we used a high order function that has a nullable type. So, whenever we do not explicitly set the closure of a specific type, this type of high order function will not satisfy the condition inside networkError.getResponse and it will direct to default.

It didn’t help that test cases were being manually tested.

Oh, and those fragments? Not everyone knew the lifecycle well (The image below still gives me headaches).

Figure 6. Android LifeCycle
Here’s a familiar crash in Fabric

java.lang.IllegalStateException: Fragment not attached to Activity

The trick to avoiding a crash is enabling ‘don’t keep activities’ and having proper instrument tests and acceptance tests, to handle the cases.

Here’s what we learnt:

1. Don’t change anything mid-sprint.
2. Every commit should have an appropriate test (both unit and instrument test)
3. Automated tests help.
4. Engineers should undertake sharing sessions to enrich knowledge.

Figure 7. Yay! Crash Free Sessions

We will discuss some of the methods we tried, what worked and what didn’t, in a subsequent post. If you found this useful, do leave feedback in the comments, and share with fellow developers!

We fail a lot at GOJEK. But we never fail to learn from our failures and do things better. It’s this mindset that has led to us becoming the largest single market food delivery app in the world, and go from being a call centre for ojeks to a Super App offering 18+ products. Head over to gojek.jobs and join us on our epic journey 🙌

www.superapp.is