Case Study
Cinemark Theatres Mobile App
Founding Co-Architect & Senior Mobile Engineer · 2013 — Present
Millions of users. 12+ releases per year.
Thirteen years. One app. Every major platform shift.
In 2013, Cinemark needed a mobile app. There were two of us. I was responsible for the iOS side; my counterpart handled Android. Together we built the shared layers, network, data, and business logic, that both platforms would run on. We used MVVMCross as our MVVM framework and MonoTouch and MonoDroid as our native wrappers, before Xamarin had even fully unified those tools under one name.
What started as a two-person project grew into a team of up to nine developers at its peak. The app grew with it, from basic showtimes and ticketing to a platform covering loyalty programs, concessions ordering, seat selection, push notifications, Apple Wallet integration, Shopify merchandise, gift cards, refund management, and more. Today the team is five developers, an API engineer, three dedicated QA testers, and one product owner.
I'm the only person on the team who was there on day one. I've left the project twice and returned twice. When complex problems surface, architectural questions, obscure platform behavior, something that's always worked a certain way but nobody knows why, I'm the person the team comes to. Sometimes a new team member stumbles across a platform enum for Windows Phone in the codebase and asks why it's there. That's a fun conversation.
The WebView problem, and why the obvious solution did not exist.
The most technically complex feature in the app, one that remains the most complex today, is the purchase flow. Cinemark undergoes strict financial audits, and the decision was made early that the mobile app should never directly handle credit card data. The solution was to route all purchase transactions through the existing mobile website, loaded inside the app via a WebView.
In practice, this created a problem that the Xamarin Forms WebView was simply not built to handle. The purchase site required custom HTTP headers for user authentication, and the default WebView did not support them. We also needed a reliable two-way communication channel: the app needed to pass session context into the WebView, and the WebView needed to signal back to the app whether a purchase succeeded, was cancelled, or failed. None of this was built in.
The default WebView was a sealed class, so it could not be subclassed or extended. We pulled the source directly from the Xamarin GitHub repository, brought it into the project, and built our own extensible version from scratch. That meant building from scratch:
- Custom renderers for iOS and Android — both platforms handle WebView behavior differently in ways that were not abstracted away
- Our own back and forward navigation
- A data-passing layer
- A callback system
That system has been in production for over a decade, handling real financial transactions. It has been carried through every platform migration since. It works, and I know exactly why, because I built it.
Migrating a live production app. 100+ screens, 3 months, zero downtime.
When the decision was made to migrate from Xamarin.iOS and Xamarin.Android to Xamarin Forms, the app was already large and in active production. Hundreds of screens, a complex shared layer, a user base that could not be disrupted.
We called a three-month feature freeze. All hands moved onto the migration effort. New feature requests stopped, with the only exceptions being critical bug fixes and anything the business deemed non-negotiable. We ran parallel branches, keeping the live codebase stable while the migrated version caught up. Pull requests that touched the live branch were reflected into the migration branch. It required discipline across the entire team.
We completed the migration in three months. The app went back into active feature development immediately after. From a user perspective, nothing changed. That was exactly the goal.
Replacing a custom auth system with OAuth2, mid-flight.
At some point in the app's history, authentication had been built on a custom system that worked but carried technical debt and did not meet modern security standards. The decision was made to migrate to OAuth2 using Keycloak as the identity provider, and MSAL.NET as the mobile authentication library.
Authentication is the kind of change that touches everything. It affects how sessions are established, how tokens are stored and refreshed, how the app recovers from auth failures, and how the purchase WebView passes identity context through to the web layer. Getting it wrong means users can't log in, or worse, don't realize something is wrong until a transaction fails.
I was the obvious choice to lead the mobile side of that effort, both because of my history with the app's authentication architecture and because of my familiarity with the WebView integration that authentication flowed through. The migration went cleanly. Authentication moved from a native screen to a popup WebView hosted by Keycloak, using the same pattern as Sign in with Google or Sign in with Microsoft. A token is returned once the user authenticates. It felt familiar because it was the same flow users already knew from other apps.
A delivery pipeline built for a team, not just a build script.
The problem
When I returned to the project, the CI/CD system had already been migrated from Visual Studio App Center to Azure DevOps by a previous developer. It worked, but it had grown into something that was increasingly difficult to maintain and scale.
The setup consisted of four separate pipelines: iOS Release, iOS Release Test, Android Release, and Android Release Test. Each pipeline built one flavor of the app for one platform. All four shared the same environment variables, XCode version, Android SDK version, .NET version, and others. That shared dependency made routine upgrades painful. Changing a shared variable risked breaking pipelines you were not intending to touch, and experimenting with pipeline changes could not be done safely in isolation.
Version and build numbers were incremented manually, which meant QA had no reliable way to distinguish between builds as pull requests landed. Everything carried the same version number until a release, making it difficult to track which build contained which fix. Tagging the main branch at release time was a manual step, easy to overlook in the middle of a busy release cycle.
What I built
I rebuilt the pipeline from scratch using Azure DevOps Pipeline Templates, consolidating all four build flavors into one pipeline. Each flavor, iOS Release, iOS Release Test, Android Release, and Android Release Test, runs as an isolated template within that pipeline. Shared variables are now managed in one place, and changes to one flavor cannot inadvertently affect another.
I also implemented automatic build number versioning. Every PR merge increments the build number consistently across both platforms and both build types. iOS and Android always carry matching version and build numbers, which gives QA a reliable identifier for every build without any manual intervention. When a release build is triggered, the pipeline automatically tags the git branch with the release version. No humans involved, no risk of forgetting.
The result is twelve or more production releases per year on a consistent monthly cadence. We monitor crash rates continuously through Sentry.io and the native store consoles, and we treat any meaningful increase as something that needs a response.
Before
- 4 separate, interdependent pipelines
- Shared variables created fragile coupling
- Build numbers incremented manually
- QA struggled to track builds across PRs
- Release tagging was a manual, forgettable step
After
- One unified pipeline using Azure DevOps Templates
- All 4 build flavors managed in isolation
- Build numbers auto-incremented per PR
- iOS and Android always share the same version
- Release branch tagging automated, no human required
What thirteen years of disciplined mobile engineering looks like.
4.9
App Store rating, 820K+ ratings
4.6
Google Play rating, 108K+ reviews
<1%
Crash rate, monitored and maintained continuously
12+
Production releases per year
The Cinemark app is a revenue channel, not just a ticketing utility. Concessions ordering is the clearest example: customers can buy tickets and add food separately as showtime approaches, instead of handling everything in one transaction at the venue. That feature launched on mobile first.
A 4.9 App Store rating with over 820,000 ratings is not a vanity metric. At that scale, it means the app works, updates without breaking things, and does not surprise its users in bad ways. You only get there by staying on a product long enough to know where all the skeletons are.
Technologies used
Let's have a conversation.
I'm not actively in the market right now, but I'm always open to conversations worth having. If something here caught your attention, I'd still like to hear from you.