HomeCase studiesA Grocery Basket Price Comparison and Ordering...

Case study · Cork, Ireland

A Grocery Basket Price Comparison and Ordering App

Retail grocery and consumer price comparison

Comparing grocery prices item by item tells a shopper nothing useful — no store is cheapest on everything. Our team built native iOS and Android applications that compare an entire assembled basket across nearby stores, handle products a store does not stock rather than quietly ignoring them, and turn the result into a single clear answer about where to shop — then let the customer order from there. Loyalty rewards, order history and location-based store discovery complete the journey.

Industry
Retail grocery and consumer price comparison
Solution
Two fully native mobile applications — Kotlin for Android and Swift for iOS — over a shared REST API, delivering basket comparison, ordering and loyalty.
Platforms
Android · iOS
Stack
Kotlin · Swift · Retrofit
Location
Cork, Ireland · delivered remotely
Role
Engineering, with the team behind for mobile and QA

Delivered remotely for a business based in Cork, Ireland. Names withheld by agreement.

Project Overview

Industry: Retail grocery and consumer price comparison.

Type of solution: Two fully native mobile applications — Kotlin for Android and Swift for iOS — over a shared REST API, delivering basket comparison, ordering and loyalty.

Business context: Grocery is one of the few categories where households spend continuously, notice price differences, and still have almost no practical way to act on them. Comparison sites report individual product prices, which is information without utility: a shopper does not buy one item, and no retailer is cheapest across a whole basket. The commercially useful question is different and considerably harder — for the specific things I am buying this week, where should I shop?

General users: Household grocery shoppers. Retailer-facing and administrative functionality lives in the backend platform rather than in these applications.

General purpose: To turn grocery price data into a decision a shopper can act on, and then to let them act on it without leaving the app.

The Business Challenge

Per-item price comparison is nearly useless. Knowing that one store is cheaper on milk and another on coffee does not tell anyone where to shop. Only the basket total answers the actual question.

Basket comparison invites a dishonest answer. Stores do not stock identical ranges. If unstocked products are simply omitted from a store's total, that store appears cheapest precisely because it carries less — the comparison becomes actively misleading, and shoppers discover this the moment they arrive at the till.

Products must be matched across retailers. Comparing a basket requires knowing which product at one store corresponds to which at another, across different packaging, sizes and own-brand equivalents. This data problem dominates the entire category.

The shopper is waiting. Pricing a full basket across several stores is much heavier than a single lookup, and it happens while the user watches. Slow comparison is unused comparison.

Insight has to convert into an order. A comparison that ends in a number is a research tool. Turning it into a completed order is what makes the product a business.

Two platforms, doubled cost. Fully native iOS and Android applications mean every feature is built twice, with a permanent risk of behavioural drift between them.

Search must feel instant across a large catalogue. Grocery catalogues are large and shoppers type partial, informal product names.

Our Approach

Compare baskets, not items. We built the comparison around the assembled basket as the unit, because that is the only comparison that answers a real question. It is the harder path — per-item comparison would have been trivial — but it is the difference between a curiosity and a product people use weekly.

Handle missing items explicitly and visibly. Products a store does not stock are surfaced through a dedicated flow rather than dropped from that store's total. This protects the integrity of the comparison, because a basket total is only meaningful if the shopper knows exactly what it does and does not include.

Turn the comparison into a decision. Rather than presenting a table of totals for the shopper to interpret, the result identifies the best-value store for their specific basket. The product's job is to answer the question, not to hand back the raw material for answering it.

Compute comparison server-side. With two native clients, duplicating pricing and comparison logic would guarantee divergence between platforms. All comparison computation happens once, on the server, with both applications rendering the same result set.

Give each screen its own API boundary on iOS. Rather than a single monolithic network manager, each screen owns a dedicated service class handling its own API calls, with separate handler classes owning data-source behaviour for list-driven screens. For a native codebase of this size, that separation is what keeps it maintainable.

Centralise transport on Android. A single generic HTTP layer covers every verb and body encoding — JSON, multipart and form-encoded — so no screen contains transport code and the API contract is adopted in one place.

Build discovery around location. Store selection uses mapping and location services, because relevance in grocery is a function of how far the shopper is willing to travel.

Support both appearances from the start. Light and dark presentation were built in rather than retrofitted, which is significantly cheaper than adding them to a finished interface.

The Solution

Location-based store discovery. Shoppers identify nearby stores through mapping and location services, establishing the set of retailers their basket will be compared across.

Fast product search. Type-ahead search returns results as the shopper types, accommodating the partial and informal product names people actually use.

Featured and promoted products. Curated and promoted listings give shoppers a route into the catalogue that does not require knowing what to search for.

Basket assembly. Products are added, adjusted and removed from a basket that persists across the session, with full product detail available before committing.

Multi-store basket comparison. The complete basket is priced across the selected stores, returning a per-store result set — the core capability the product exists to deliver.

Missing-item handling. Where a store does not stock an item in the basket, the shopper is told explicitly rather than seeing a total that quietly excludes it, so every comparison is like-for-like or clearly labelled as not.

A clear best-value result. The comparison resolves into an identified best-value store for that specific basket, presenting a decision rather than a dataset.

Checkout. The shopper orders from the chosen store, converting the comparison insight into a completed transaction without leaving the app.

Order history. Previous orders are retained and re-accessible, which matters disproportionately in grocery — where most of each week's basket repeats the last one.

Loyalty and rewards. A rewards programme layered over ordering, giving shoppers a reason to transact in-app rather than using the comparison and then buying elsewhere.

Account and profile management. Registration, sign-in, password management, profile details, notification handling and settings.

Key Features

Whole-basket price comparison The complete assembled basket is priced across multiple nearby stores, answering the question shoppers actually have rather than reporting individual item prices.

Explicit missing-item handling Products a store does not stock are surfaced through a dedicated flow rather than silently excluded, protecting the comparison from systematically favouring stores with the narrowest range.

Best-value result presentation The comparison resolves into an identified best-value store, giving the shopper a decision instead of a table to interpret.

Location-based store discovery Mapping and location services establish which nearby stores are relevant, since travel distance determines what a saving is actually worth.

Type-ahead product search Incremental search across a large grocery catalogue, tolerant of the partial and informal terms shoppers use.

Integrated checkout Ordering from the chosen store inside the app, converting comparison insight into a completed transaction.

Order history and repeat access Retained previous orders, reflecting how much of a weekly grocery basket repeats — turning a comparison tool into a weekly habit.

Loyalty rewards programme Rewards for in-app transactions, aligning the shopper's incentive with completing the order rather than comparing and leaving.

Fully native on both platforms Kotlin and Swift applications built natively for their respective platforms, with server-side comparison ensuring both present identical results.

Light and dark presentation Both appearances supported from the outset rather than added afterwards.

Technical Architecture

iOS client. A native Swift application organised into UI, controllers, managers, models, helpers and extensions. Each screen pairs a view controller with a dedicated service class owning that screen's API interaction, and list-driven screens have separate handler classes owning data-source behaviour — a deliberate three-way separation that keeps a large UIKit codebase maintainable.

Android client. A native Kotlin application organised by concern — UI, networking, adapters, feature-grouped models, utilities, extensions and interfaces — built with a modern Gradle configuration using the Kotlin DSL and a centralised dependency version catalog.

Networking layer. On Android, a single generic HTTP client exposes every verb and body encoding through one interface, with a typed API contract layered over it. On iOS, per-screen service classes own their own requests. Both target the same REST API with token-based authorisation.

Comparison engine. Basket comparison is computed server-side and returned to both clients as a per-store result set, so pricing logic exists once rather than being duplicated across two native codebases.

Location and mapping. Platform location services and mapping integration drive store discovery and relevance.

Media handling. Image loading with caching on both platforms, necessary for the heavily image-driven product listings a grocery catalogue requires.

Flow: Native app (iOS / Android) → Token-authenticated REST API → Server-side basket comparison across stores → Per-store result set with missing-item detail → Best-value presentation → Checkout → Order history and loyalty

Technology Stack

Category Technology
Android language Kotlin
Android build Gradle with Kotlin DSL and dependency version catalog
Android networking Retrofit with Gson, OkHttp with logging interceptor, over a generic HTTP request layer
Android UI AndroidX, Material Components, ConstraintLayout, with light and dark theme resources
Android media Glide image loading and caching
iOS language Swift
iOS architecture Per-screen view controller + service class, with dedicated handlers for list-driven screens
iOS dependencies CocoaPods (rich text labels, page control, animated image support)
Location & mapping Google Play Services Maps and Location on Android; platform location services on iOS
API Shared REST API with token-based authorisation
Comparison Server-side basket comparison returning per-store results

Technical Challenges & Solutions

Challenge Our Approach
Per-item price comparison being commercially useless Comparison built at whole-basket level as the product's core unit — the harder engineering path, but the only one that answers the question a shopper actually has.
Basket totals being distorted by products a store does not stock Missing items surfaced through an explicit dedicated flow rather than dropped from the total, so a comparison is either genuinely like-for-like or clearly labelled as not. Without this, the comparison would systematically reward the store with the poorest range.
Shoppers receiving numbers instead of an answer The comparison resolves into an identified best-value store for that specific basket, presenting a decision rather than a dataset for the user to interpret.
Two native codebases risking divergent results All comparison and pricing computation performed server-side and returned as a result set, so the logic exists once and both platforms are guaranteed to present the same answer.
A large grocery catalogue and informal search terms Type-ahead search with incremental querying, returning results as the shopper types rather than requiring complete, correctly formed product names.
Screens accumulating transport and data-source logic On iOS, each screen owns a dedicated service class and, where list-driven, a separate data-source handler; on Android, a single generic HTTP layer means no screen contains transport code.
Image-heavy listings on mobile connections Caching image loaders on both platforms, so repeated browsing of a large product catalogue does not repeatedly re-fetch the same media.
Comparison insight not converting to revenue Checkout, order history and a loyalty rewards programme built into the same journey, so the shopper transacts in-app rather than comparing and buying elsewhere.

Security & Reliability

Authenticated API access. Requests carry token-based authorisation, with registration, sign-in and password management handled through the platform API.

Server-side computation. Pricing and comparison logic is held server-side rather than in the clients, so the values a shopper sees cannot be manipulated on the device and both platforms are guaranteed consistent.

Location permissions. Location access is requested for store discovery through the platform permission model on each operating system.

Data minimisation on device. The applications are presentation clients over a hosted API rather than local systems of record, so order, pricing and account data is not accumulated on the device.

Consistent transport handling. Centralised networking layers on both platforms mean authorisation, encoding and error handling are applied uniformly rather than re-implemented per screen.

Scalability & Performance

Server-side comparison. The heavy computation — pricing a full basket across several stores — happens once on the server, so client performance is a rendering concern rather than a calculation one.

Incremental search. Type-ahead querying returns bounded result sets as the shopper types, rather than loading or filtering a large catalogue on the device.

Image caching. Cached image loading on both platforms keeps repeated catalogue browsing fast and reduces data usage on mobile connections.

Efficient list rendering. Dedicated data-source handling for list-driven screens keeps large product and comparison lists smooth.

Native performance. Fully native implementations on both platforms give direct access to platform rendering, location and media capabilities without an abstraction layer.

Thin clients. With the comparison engine and catalogue held server-side, the applications remain light and their behaviour scales with backend capacity rather than device capability.

Business Outcomes

  • Shoppers get an actionable answer, because comparison operates on the whole basket rather than on individual products.
  • The comparison can be trusted, since unstocked items are surfaced explicitly instead of quietly improving a store's total.
  • Insight converts into revenue, with checkout, order history and loyalty rewards built into the same journey rather than left to a separate channel.
  • Repeat shopping is frictionless, through retained order history — significant in a category where most of each week's basket repeats the last.
  • Both platforms always agree, because comparison is computed once server-side rather than implemented twice.
  • Store relevance reflects reality, with location-based discovery ensuring compared stores are ones the shopper would actually visit.
  • The apps feel native, because they are — with full platform integration and both light and dark presentation.

Why it worked

Price comparison products fail on integrity long before they fail on features. It is trivial to produce a basket total that makes one store look cheapest; it is considerably harder to produce one a shopper can trust after they have been to the till. That difference is entirely an engineering and product-judgement question — how unstocked items are handled, whether products are genuinely matched, whether the answer is presented as a decision or as raw data.

Our team builds for the version that survives contact with the customer. We chose basket-level comparison over the far easier per-item approach because only one of them is worth using. We made missing items an explicit, visible flow rather than a silent omission. And we kept the comparison computation server-side, because two native clients implementing the same pricing logic will diverge — and in a comparison product, two platforms disagreeing about which store is cheapest is fatal to trust.

Our teams work natively across Kotlin and Swift as well as cross-platform, and structure large native codebases so they stay maintainable — per-screen service boundaries, separated data-source handling, and centralised transport, rather than the accumulation of network code in view controllers that makes most apps of this size expensive to change.

Final Summary

Grocery shoppers know prices differ between stores and have almost no practical way to act on it. Comparison tools report individual product prices, which sounds useful and is not: nobody buys one item, and no retailer is cheapest across a full basket. The question worth answering is where to shop for this basket, this week — and answering it honestly is harder than it looks, because stores stock different ranges and a naive total rewards whichever store carries the least.

Our team built native iOS and Android applications around that harder question. Shoppers discover nearby stores by location, search a large catalogue with type-ahead, assemble a basket, and have that complete basket priced across multiple retailers. Items a store does not stock are surfaced explicitly rather than dropped from the total, so every comparison is either genuinely like-for-like or clearly flagged. The result resolves into an identified best-value store — a decision, not a spreadsheet — and the shopper can order from it immediately, with order history and loyalty rewards keeping the habit in the app.

Technically, the significant decisions were to compute comparison server-side so two native codebases could never disagree about the answer, and to structure both applications so they stay maintainable at scale — per-screen service boundaries and separated data-source handling on iOS, a single generic transport layer on Android. In a comparison product, trust is the entire proposition, and trust is what those decisions protect.

01 — Questions

asked about this kind of project

How do you build a price comparison app that people actually use?

Compare baskets, not items. Individual product prices are easy to display and tell the shopper nothing actionable, because no retailer is cheapest on everything. The whole basket is the only unit that answers a real question, and building the comparison around it from the start — rather than as a feature layered onto per-item data — is what separates a product people use weekly from one they open once.

How do you handle products a store does not stock in a basket comparison?

Surface them explicitly. If unstocked items are simply excluded from a store's total, the comparison systematically favours whichever store carries the narrowest range, and shoppers discover the discrepancy at the till. A dedicated missing-item flow tells the shopper exactly what a total does and does not include, which is the difference between a comparison they trust and one they abandon.

Should comparison logic live in the app or on the server?

On the server, particularly with more than one client. Duplicating pricing logic across native iOS and Android codebases guarantees they eventually diverge — and two platforms disagreeing about which store is cheapest destroys the product's credibility. Server-side computation also means values cannot be manipulated on the device, and pricing rules can change without an app release.

Native or cross-platform for a retail shopping app?

Both are viable; the decision usually turns on team structure and platform-specific requirements rather than capability. Native gives the deepest platform integration and the best performance ceiling, at roughly double the build cost. Cross-platform halves ongoing cost and keeps features in step across platforms. Where two native codebases are used, keeping shared business logic server-side is essential to prevent behavioural drift.

How do you keep a large native codebase maintainable?

Give each screen its own API boundary rather than routing everything through one monolithic network manager, and keep data-source logic out of view controllers by separating it into dedicated handlers. Centralise transport so no screen writes networking code. These are unglamorous structural decisions, and they are the difference between a codebase where the tenth feature costs the same as the third and one where it costs three times as much.

How does product matching work across different retailers?

It is the hardest problem in this category and it is fundamentally a data problem rather than an app problem. Products must be resolved to a common identity across differing packaging, sizes, formats and own-brand equivalents, typically through a combination of standard product codes, attribute matching and human curation for ambiguous cases. The quality of that matching determines whether the comparison is meaningful at all.

How do you turn a comparison tool into a business?

Complete the journey. A comparison that ends in a number is a research utility that sends its value to whoever fulfils the order. Building checkout, order history and loyalty rewards into the same product means the insight converts into a transaction — and in grocery specifically, retained order history is unusually powerful because most of each week's basket repeats the last one.

What should be built first in a comparison app?

The data foundation and the comparison itself, before the surrounding features. Catalogue, product matching and honest basket comparison are what the product is; search, loyalty and order history are what make it a habit. Getting a trustworthy comparison in front of real shoppers early matters most, because a comparison that turns out to be wrong at the till is the one failure the product cannot recover from.

03 — Similar project?

describe what is different about yours

Build yours.

Tell me what exists, what you need and the deadline. Fixed price for a defined scope, or hourly from $10 with a written estimate first.

Ahmedabad, India · IST (UTC+5:30) · --:-- IST · Mon–Fri 09:00–18:00 IST · US & EU overlap daily

No newsletter, no CRM. Just a reply.