Organising the Singpass app Inbox for close to 4 million users (Part 3 — Engineering)

Terence Peh
NDI.sg
Published in
12 min readDec 22, 2022
Photo by Glenn Carstens-Peters on Unsplash

Notify, also known as Inbox on the Singpass app in its humble beginning had started with the design philosophy of simplicity and minimalism in mind, as there weren’t too many messages in the beginning. Initially, we didn’t know how many types of notifications it may evolve into, therefore the team tried not to add too many controls to it.

What this translated to from the mobile app engineering perspective was a couple of APIs to fetch the messages and update the message status, and a single database table that represented the message content. We ensured that we designed the Inbox with an offline-first approach and prioritised usability when designing the UI. Let’s run through some of the original features in the Inbox before the revamp.

Going down the Memory Lane

Inbox started with only two main views, a compact list view of the messages and a detailed message view displaying the message payload. There are also the sub-views such as a call-to-action status view.

Inbox view

The compact list view shows essential information to the user, such as title, short message, read status, and date-time.

Inbox main view before the revamp

Inbox detail view

The message detail view started with displaying just plaintext, which can be tedious to read. Over the iterations, it has evolved to support different text formats according to the message templates received.

  • Text
  • Rich Text
  • HTML

The detail view also supports displaying Call To Action buttons that can be linked to external and internal app functionalities. There aren’t any changes to the detail view in this revamp.

Inbox message detail view before the revamp

Controls

We also kept inbox management controls to the minimum, with few basic control actions found in most mailboxes.

  • Mark messages read
  • Delete messages
  • Delete all messages

The changes

Photo by Ross Findon on Unsplash

A brief recap: The motivation for the Inbox revamp is to address users’ pain points of a rather cluttered inbox with the growing number of messages sent out to users through our Notify service. This is due to the increased number of campaigns from various government agencies. Notable topics include Budget 2022 and, more recently, educational campaigns due to increased cybersecurity threats from scams and phishing attacks.

In the following sections, we will dive deep into the journey of the revamp in chronological order from the app’s perspective. We will cover the challenges faced and talk about the thought process of the engineering team during the revamp.

Inbox update campaign message

Initially, we intended to release the changes in one go within back-to-back sprints, but due to the technical constraint from both the backend and frontend teams and our release cycles, we broke it up into phases. The changes were rolled out in two main phases, phases 1 and 2.

Phase 1: Backend search

The team chose to implement and release the backend search first as we wanted to release something quickly to alleviate our users’ pain points immediately. The initial assessment during our backlog grooming shows that it has the least complexity and changes to existing data object models on both the app and the backend. The changes on the backend involve enhancing the existing API to take in a search term and return a response for the given search term.

Inbox search landing view

We initially intended for the search feature to be online-only such that any action performed within the search screen should be isolated, independent of the local cache in the app. This helps reduce complexity, or so we thought.

Inbox search result view

Controls

We needed to include message actions (i.e., mark as read, delete, delete all) within the search screen, despite our initial assumption that we would only display the results. During sprint planning, the team discovered these message actions to be part of the search view only after reviewing the UI mockups. Actions performed by users within this screen would have to be synchronised with the local cache. Therefore, we had to make additional changes to these existing controls to accommodate the ability to perform actions on this list.

Android selected messages’ menu options

Fortunately, there weren’t any API changes required as the app could workaround the existing APIs using the IDs of the message object model.

We also took the opportunity to take a look at the overall Inbox code and managed to refactor it in preparation for the upcoming features in phase 2. Code clean-up for unwanted attributes, and other legacy code left behind over the feature’s iterations since its inception, was done progressively over the two phases.

Overall, our initial assessments of the backend search were not too far-off, and we rolled out the backend search feature with minimal “surprises” that we had to deal with.

Phase 2a: Revised categories

The revamped UI has a pill-like control to filter messages based on their category. This aims to help reduce the clutter on the main Inbox screen.

Empty state of the filtered Inbox’s main view

Initially, these revised categories were supposed to be introduced together with the backend search. We thought adding a pill-like Material Chips control below the search bar to filter messages would be a minor change. We soon realised we would have to deal with existing messages that had already been broadly categorised as “Messages”.

For context, the initial Inbox only had two categories, “Announcements” and “Messages”. For filtering to work, there will be a larger change beyond the technical aspects.

What are the new categories? What’s our migration strategy? How would we deal with existing messages that have already been assigned a category? We dropped the idea of releasing this with the backend search in phase 1 so that we could rethink a more effective filtering and migration strategy, which later evolved into the revised categories we have today.

Caching

The work on revised categories started early during phase 1 for specific tasks that could be done without dependencies, such as creating UI components. In phase 2, categories evolved into a new API call so that the app could show the category names and unread count within each category pill. With a new API comes additional caching as the Inbox works on an offline-first approach. This allows users to access their cached messages in situations such as when they don’t have Internet access while traveling and transiting.

There is also the need to synchronise the category pills with the updated categories and unread count as users may receive new messages from push notifications within different views of the Inbox feature. We designed the category pills with dynamic categories in mind so that whenever we add new categories, the Inbox feature can handle them well.

The larger part of the work involving revised categories eventually evolved from filtering to threaded messages, called topics. This resulted in two different layered views, with topics (a group of messages) and messages. We will explain threaded messages in detail in the next section.

Phase 2a: Threaded messages

The largest change in the revamp for both frontend and backend teams would be threaded messages (also known as topics internally), even though visually it doesn’t look like a lot had changed.

Inbox topics view

The immediate questions that the team had when we first saw the UI mockups were:

  • How do we group messages efficiently?
  • How do we count messages within each topic?
  • How do we treat topics with only a single message?
  • How do we handle pagination?

Messages are grouped according to the topic group assigned to them by the backend based on the newly revised categories. The backend team did most of the heavy lifting of grouping messages into the suitable topics. From the UI’s perspective, topics with more than one message will be displayed with the topic title, unread count badge, and information from the latest message. Hence, we created a new API for the app to retrieve the list of topic items in the topic view.

There is also a difference in displaying topics with only a single message on the UI, as it will look just like an individual message item instead of a topic item. This means that the topic item returned from the API should contain information on the total count of messages within each topic group for the app to display the correct information. This new topic information also needs to be cached to maintain the offline-first approach of the overall Inbox feature.

Inbox topic view scrolled

Caching

On the app, to minimise changes needed to the existing message object model and avoid duplicating data, the team took the approach of making the new topic object model a separate entity from the message object. This new topic object is separately stored in a new table.

The existing message object is embedded into the topic object to create a relationship between the topic and the latest message within the topic item. This allows the app to cache only the data related to the topic while maintaining the latest message data cache in the existing message table.

This approach also helped the backend improve the new topic API’s performance by reducing the number of queries on the message table, as certain data, such as the message category and call-to-action information, are omitted from its response. The existing message API is only called on-demand after a user clicks on a topic item or views the message details, in the case of a topic with a single message.

Threaded message view

Backward compatibility

It was not feasible to migrate all the existing Inbox messages into topics due to the large number of records in the database. Patching all these existing messages into different topic groups at once will not be possible. After a few discussions, the team decided to assign historical messages in the Inbox without topics to a legacy topic group we called “Previous Messages”.

Inbox topic view with “Previous messages”

Controls

We also duplicated the existing actions in the message view on the topic view for consistency. A mixture of new APIs and enhancements to existing APIs were completed to accommodate this change, allowing actions such as deleting or marking all messages within the same topic group as read.

iOS 13 context menu for control actions

The most notable change was to return category information together with the action API so that the category pills can be updated if there were messages marked read. This reduces the number of calls needed to the category API. There is one exception: the category API will be called when a new push notification is received so that the app can synchronise the unread count for the various categories.

Phase 2a: Notification settings

The idea of having in-app notification settings was driven by the feedback we received from users. They could not control notifications for messages they received. This was made apparent when users received more messages informing them of phishing attempts and scams, yet not all users would want to be constantly bugged by it.

On Android, the revised categories could be mapped into different notification channels such that the individual categories can be turned on or off according to the user’s preference. For iOS, however, there is no concept of notification channels. Furthermore, certain notification categories that require the user’s action should not be disabled. Considering these limitations, we decided to build a custom in-app notification settings page to best suit both platforms.

Notification settings view

The notification settings screen is relatively straightforward, with an API to update the backend on the user’s notification preference according to the revised categories. It is also tightly integrated with the user’s device notification settings. We will inform the user and link them to the appropriate page whenever it is turned off.

Notification settings view when device notifications are turned off

There are also multiple entry points to the in-app notification settings. For example, a user could find it from the app’s main settings screen or the Inbox menu options. A “promo” banner is also shown to users entering the Inbox for the first time.

Notification settings “promo” banner

On Android, we also looked into the new notification runtime permissions required for Android 13 and implemented them based on the recommended best practice. This means we’ll only ask for notification permissions in-context when needed.

Phase 2b: Pagination and Pinned messages

There was a minor twist of events due to the added complexity of offline handling for the control actions, which we discovered during the development phase for threaded messages. As threaded messages were an important feature, we afforded more time to ensure the feature met the requirement. We had to deprioritise pagination and pinned messages to meet our original development target.

The initial design of the Inbox topic view allowed a user to pin individual messages, which could either be a topic or a message. Yet, there is a technical constraint trying to paginate both topics and actual messages simultaneously, as these were two different data types. This isn’t possible, as we have discovered during sprint execution. We therefore decided to have the backend return the single message as a topic object, and the app would then display it accordingly on the UI.

With the change, pagination became possible for topic items in the topic view. The app would provide the page key and size as query params to the topic API, and the response would contain the page key of the next available page if more pages were available.

Inbox topic empty state for pinned tab

The Pinned messages feature was initially designed for pinning topics and individual messages. However, pinning a topic with multiple messages doesn’t pin all the individual messages within. Given the pagination constraints above, the pin feature would eventually change to only pinning topic items since that is the only item shown on the actual topic view screen.

Swipe on a message item to reveal available actions, such as Pin

Accessibility

Our designers have always been strong advocates for accessibility and inclusion to create an app with an exceptional experience for both abled and less-abled people. In this revamp, we made sure accessibility was included in the “Definition of Done” in our sprints. We have also iteratively refined and improved the Inbox based on feedback received from user studies. Further, we’ve also customised our UI to enhance the readability for users using Apple VoiceOver or Android TalkBack and fixed several elements with incorrect/missing content descriptions.

Ending notes

The Inbox revamp is generally well coordinated and allowed us to achieve our desired outcomes. Sure, there were instances where gaps were only identified during development, but that’s commonly what a Scrum team encounters. Our teams managed to inspect the gaps, identify the additional work necessary, adapt to the changes, and overcome them quickly to achieve the sprint goal. This is probably the first time cross-functional teams representing the backend and frontend worked on development back-to-back concurrently in the same sprint, making it close to a “true” Scrum approach.

Overall, this is a great opportunity for the team to clean up and modernise a three-year-old feature on both platforms. On Android, we were able to adopt and experiment more with the new Jetpack Compose to take advantage of the library’s improved performance. On iOS, we did a major re-architecture and code refactoring of the Inbox feature in preparation for the revamp.

This concludes the three-part series. If you have missed the earlier parts, read Part 1 (UX Research) and Part 2 (UX Design).

Team credits: Melvin Tan (Lead), Chin Wee Koh (iOS Dev), Law Xun Da (iOS Dev), Kenneth Leong (Android Dev), Terence Peh (Android Dev), Tay Li Soon (Product Manager + Chief Editor) and all the engineers involved

--

--