cswork / libraryOpen practice workspace

Interview guides · Aston Carter

Aston Carter Software Engineer Interview Guide

Aston Carter Software Engineer interview guide: Java, Kotlin, Android, concurrency and product design questions with answer approaches and a 7-day plan.

The 60-second brief

Aston Carter is a staffing and workforce solutions company. Candidates describe the engineering work as building the applications that recruiters, client companies and job seekers use: job applications, workforce logistics, and communication between the parties. Much of it is reported to be mobile and cross-platform work in Java and Kotlin on Android, backed by server integrations that move a lot of data and run many operations at once. For a Software Engineer this means shipping features end to end on the client, keeping background work off the UI thread, modernising older Java code, and weighing in on whether a feature is technically feasible before it is built.

This guide is for engineers interviewing for the Software Engineer role at Aston Carter, especially those with a mobile or backend-leaning Java and Kotlin background. The requirements candidates describe list strong Java and Kotlin, Android or other mobile framework experience, concurrency and memory management, and roughly three or more years shipping mobile or full-stack software; CI/CD for mobile, Flutter or React Native, and HR-tech experience are listed as extras. It covers the three reported rounds, answer approaches for every reported question (language, Android runtime, concurrency, design and behavioral), the mistakes that cost points on those questions, and a day-by-day plan.

Candidates report three rounds over about three to five weeks: a recruiter screen, technical rounds centred on Java, Kotlin and Android, and a final stage of advanced technical questions, concurrency and product-focused behavioral questions.

Focus areas

  • Concurrency
  • Java
  • Kotlin

Overview

The reported questions fall into three clusters. The first is language and platform knowledge: why move a codebase from Java to Kotlin, what val and var really guarantee, how the Activity lifecycle interacts with memory, how garbage collection causes dropped frames, and how dependency injection works. The second is concurrency on Android: background work, deadlocks, coroutines versus RxJava, main-thread UI updates and volatile. The third is product-led design: a feature for a music app with its backend, offline sync for large data, diagnosing a slow app, and messaging inside a job-search app, plus one behavioral prompt about pushing back on a requirement.

Candidates describe the technical rounds as conversations about how the tools work rather than algorithm puzzles. That changes how you prepare. Instead of grinding array problems, build answers that go one layer below the API: what the compiler emits for a val, what ART does during a collection, which thread a Dispatcher runs on, which lock each thread holds in a deadlock. For every topic, have one short code example you can write from memory and one production story where it mattered.

Concurrency appears in both the reported questions and the final-stage description, so treat it as the area to over-prepare. You should be able to explain the Java memory model's visibility and happens-before guarantees, structured concurrency and cancellation in coroutines, the RxJava threading operators, and how WorkManager, coroutines and foreground services split the background-work problem on Android.

The design questions are product questions first. Each one is easier if you open with the user and the problem, pick one concrete scope, then move to the data model, the sync or delivery mechanism, failure handling and how you would measure success. Since the role sits in a staffing business, the messaging and offline-sync prompts reward answers that think about recruiters and candidates as distinct users with different permissions.

The Aston Carter interview process

Three stages are reported, in this order. The descriptions below summarise what candidates say about each, followed by how to prepare for it.

Recruiter Screen

Candidates describe an opening conversation with a recruiter that checks your background, your expectations and whether your technical stack fits the role. For this role the stack question is concrete: Java, Kotlin and Android or other mobile work. Treat it as a fit check you can steer. Lead with the Kotlin and Android work you have shipped, say plainly how much of it was Java versus Kotlin, and use the call to learn whether the team leans mobile or backend, how the later rounds are run, and what the location arrangement is, since candidates report remote and hybrid hiring in several regions.

What to demonstrate

  • Alignment between your background and the role
  • Fit with the Java, Kotlin and Android stack
  • Expectations and logistics such as location and timing

How to prepare

  • Write a two-minute summary built around one shipped Android or Kotlin project: what it did, your part, and one concurrency or performance problem you solved.
  • Write down your Java-to-Kotlin ratio and your years of mobile work so you can answer stack questions in one sentence each.
  • Prepare questions on team focus (mobile or backend), the format of the technical rounds, and the location or remote arrangement.

Technical Rounds

Candidates report in-depth technical discussions on their areas of expertise, naming Java, Kotlin and Android development, and describe them as conversations about how things work rather than puzzle-style whiteboard problems. The reported language and platform questions (Java to Kotlin migration, val versus var, the Activity lifecycle, garbage collection and UI stutter, dependency injection) are the material to practise for this stage. Answer each at two levels: the API behaviour, then what happens underneath, and anchor at least one point per answer in something you have shipped. Expect to justify choices such as Kotlin over Java or Hilt over manual injection with concrete reasons.

What to demonstrate

  • Depth on Java and Kotlin language semantics and interop
  • Understanding of the Android lifecycle, memory management and app architecture
  • Ability to explain and defend technology choices

How to prepare

  • Rehearse the lifecycle callbacks in order, then explain what a configuration change and process death each destroy and how ViewModel and SavedStateHandle cover them.
  • Write a small Kotlin file showing val holding a mutable list, a data class with copy(), nullable types and an extension function, and explain each line aloud.
  • Explain generational GC and ART's concurrent copying collector, then name three allocation hot spots that cause jank and the fix for each.
  • Wire a ViewModel with Hilt and with manual constructor injection, and list the trade-offs between them.

Advanced Technical Concepts

Candidates describe the final stage as a mix of advanced technical questions and product-focused behavioral questions, with concurrency and multithreading scenarios alongside open-ended prompts to come up with and design app features. The reported concurrency questions (background tasks, deadlock, coroutines versus RxJava, main-thread updates, volatile) and the product and design questions (music app feature, offline sync, a slow app, messaging in a job-search app, pushing back on a requirement) are the material for this stage. Switch modes cleanly: precise memory-model language for thread safety, then user-first structure for product prompts.

What to demonstrate

  • Correct reasoning about thread safety, deadlock and asynchronous code
  • Feature ideation and turning a product idea into an architecture
  • Trade-off judgement and communication in product-focused behavioral questions

How to prepare

  • Write the two-lock deadlock in Java from memory, then fix it with lock ordering and with tryLock, and explain how a thread dump reveals it.
  • Explain the same network-then-UI flow three ways: coroutines with withContext, RxJava with subscribeOn/observeOn, and WorkManager for deferrable work.
  • Outline the offline-sync and messaging designs on paper: data model, sync or delivery path, conflict or ordering rule, and failure cases.
  • Prepare a STAR story about pushing back on a requirement, with the constraint quantified and the alternative you offered.

Where candidates lose points

Answering the Java and Kotlin questions at syntax level, for example saying val makes an object immutable or that Kotlin removes all NullPointerExceptions.

For each language question prepare the layer underneath: val as a final reference that can still point to a mutable list, platform types from Java breaking null safety, what a data class generates. Say the caveat before you are asked for it.

Treating thread safety as an API list: using volatile for a counter, calling coroutines threads, or describing a deadlock without naming which lock each thread holds.

Practise the reported concurrency set with code from memory: the two-lock deadlock and its lock-ordering fix, a volatile stop flag versus AtomicInteger, structured concurrency and cancellation, and how the work reaches the main thread. Use happens-before and visibility vocabulary correctly.

Opening the music-app or messaging prompt with a database schema before saying who the user is or what problem the feature solves.

Use a fixed order for every product prompt: user and problem, one scoped feature and its success metric, then data model, delivery or sync path, failure cases and how you would measure the launch.

Designing offline sync as 'retry when the network comes back', with no idempotency, ordering or conflict rule.

Always name the outbox with client-generated ids, server-side idempotency, resumable chunked uploads, cursor-based delta download and a conflict rule based on server versions. Then walk one failure: the app is killed halfway through a batch.

Answering the slow-app question with a list of optimisations before any measurement.

Start by narrowing which kind of slow and for whom, measure with traces and percentiles, reproduce on a low-end device, then fix the cause the profile shows and prove the change with the same benchmark.

Questions & practice

Explain the Android Activity lifecycle and how it impacts memory management.

Medium · Technical and Domain Knowledge (Java/Kot

Approach

  1. List the callbacks in order: onCreate, onStart, onResume, then onPause, onStop, onDestroy (onRestart when returning from stopped). Say which state is visible, which is foreground, and that the system may kill a stopped app's process.
  2. Explain the two destruction cases: a configuration change destroys and recreates the Activity, while process death kills everything. ViewModel survives the first; onSaveInstanceState or SavedStateHandle survives both, with small data only.
  3. Name the leak pattern: anything that outlives the Activity and holds a reference to it (static fields, singletons holding an Activity Context, inner-class Runnables or callbacks, unregistered listeners) keeps the whole view tree from being collected.
  4. Give the fixes: application Context for long-lived objects, lifecycle-aware collection (repeatOnLifecycle, LifecycleObserver), cancel work in viewModelScope, unregister in the matching callback, and null view binding in a Fragment's onDestroyView.
  5. Close with detection: LeakCanary in debug builds, a heap dump in the Android Studio Memory Profiler, and onTrimMemory to release caches under pressure.

Follow-up

  • How do you handle a configuration change without losing state, and what is still lost after process death?
  • Why does a Fragment have a separate view lifecycle, and what leaks if you ignore it?
  • Walk through how you would find and fix a memory leak reported in production.

How does garbage collection work in Java, and how can it cause UI stutter in mobile apps?

Medium · Technical and Domain Knowledge (Java/Kot

Approach

  1. Explain reachability: the collector marks objects reachable from GC roots (thread stacks, static fields, JNI references) and reclaims the rest, so cycles are collected and a leak means something still reachable.
  2. Explain generations: most objects die young, so new objects go to a young space that is collected often and cheaply; survivors are promoted to an old space collected less often. On HotSpot this is eden and survivor spaces plus old gen.
  3. Say that Android runs ART, not HotSpot: since Android 8 it uses a concurrent copying collector, made generational in Android 10, so pauses are short but GC threads still compete for CPU and heavy allocation triggers frequent collections.
  4. Link to stutter: at 60 Hz a frame has about 16 ms. Allocating in onDraw, onBindViewHolder, scroll or animation callbacks creates churn; collections plus pauses push frames past the deadline. Fix by preallocating Paint and Rect, avoiding autoboxing (SparseArray, primitive arrays), reusing bitmaps and objects.
  5. Show how to prove it: allocation tracking in the Memory Profiler, GC events in a Perfetto trace aligned with janky frames, and a before/after jank measurement.

Follow-up

  • What is the difference between memory churn and a memory leak, and how does each show up in a profile?
  • When would you use a WeakReference or SoftReference, and why are they not a fix for leaks?
  • Why are finalizers discouraged, and what replaces them?

What is the difference between val and var in Kotlin, and how does it relate to immutability?

Medium · Technical and Domain Knowledge (Java/Kot

Approach

  1. val is a read-only reference assigned once (compiled to a final field for properties); var can be reassigned. Neither says anything about the object the reference points to.
  2. Show the trap: val items = mutableListOf<String>() can still be mutated. Even List<T> is only a read-only view; the backing list can change, or a caller can cast it to MutableList.
  3. Add the subtle case: a val with a custom getter can return a different value on every call, so val means no setter, not a constant. const val is for compile-time constants of primitives and String at top level or in an object.
  4. Explain how to get real immutability: data classes with only val properties of immutable types and copy() for changes, persistent collections from kotlinx.collections.immutable, and exposing StateFlow while keeping MutableStateFlow private.
  5. Tie it to concurrency: immutable objects can be shared across threads without locks, and final fields get safe-publication guarantees under the Java memory model.

Follow-up

  • Is a val property thread-safe? When is it not?
  • When would you use lateinit var versus by lazy?
  • Why expose StateFlow instead of MutableStateFlow from a ViewModel?

How do you manage background tasks in an Android application?

Medium · Concurrency and Multithreading

Approach

  1. Classify the work before naming an API: is it tied to the screen, deferrable but must finish, user-visible and ongoing, or at an exact time?
  2. Screen-bound work: coroutines in viewModelScope or lifecycleScope, I/O on Dispatchers.IO, results applied on Main; cancellation follows the scope so nothing leaks when the user leaves.
  3. Work that must survive app exit or reboot (uploads, sync): WorkManager with constraints such as network or charging, retries with exponential backoff, enqueueUniqueWork to avoid duplicates, and chaining for multi-step jobs.
  4. User-visible ongoing work such as playback or a long upload with progress: a foreground service with a notification and the declared service type newer Android versions require. Exact-time work: AlarmManager, which is restricted.
  5. Name the platform limits: background execution limits since Android 8, Doze and App Standby, and that AsyncTask and IntentService are deprecated, which is why legacy Java code often needs migrating.

Follow-up

  • How do you guarantee an upload completes if the user swipes the app away?
  • How do you show WorkManager progress in the UI?
  • How do you stop the same sync job from being enqueued twice?

What are Kotlin Coroutines, and how do they handle concurrency differently than RxJava?

Medium · Concurrency and Multithreading

Approach

  1. Define coroutines: suspend functions compiled into state machines with continuations; suspending frees the thread, so thousands of coroutines can share a small pool. Dispatchers.Main, IO and Default pick the threads and withContext switches between them.
  2. Explain structured concurrency: every coroutine belongs to a CoroutineScope, the parent waits for its children, cancelling the parent cancels them, and a failing child cancels its siblings unless you use a SupervisorJob. Cancellation is cooperative at suspension points.
  3. Describe RxJava: a push-based stream library (Observable, Flowable, Single, Maybe, Completable) with a large operator set; threading via subscribeOn and observeOn; lifetimes managed by Disposables, often a CompositeDisposable; backpressure via Flowable.
  4. Compare fairly: coroutines give sequential-looking code, first-class Jetpack support and automatic lifetimes; Rx gives a richer operator set and is already common in older Java code. Flow is the Kotlin stream counterpart (cold), with StateFlow and SharedFlow for hot state.
  5. Give a migration path: the kotlinx-coroutines-rx3 adapters (await, asFlow, rxSingle) let you convert one layer at a time, usually the repository first.

Follow-up

  • What happens to an exception thrown in launch versus async?
  • How does cancelling a coroutine scope differ from disposing an Rx subscription?
  • How would you convert a debounced search built with RxJava to Flow?

How do you ensure that UI updates only happen on the main thread?

Medium · Concurrency and Multithreading

Approach

  1. State the rule: the Android view system is single-threaded, and touching a view from another thread throws CalledFromWrongThreadException in most paths. Where it does not throw, the update is still unsafe.
  2. List the mechanisms: Handler(Looper.getMainLooper()).post, Activity.runOnUiThread, View.post, LiveData.setValue on Main or postValue from any thread, withContext(Dispatchers.Main), and observeOn(AndroidSchedulers.mainThread()) in Rx.
  3. Show the structure that makes it automatic: the repository does work on Dispatchers.IO, the ViewModel exposes StateFlow, and the UI collects inside lifecycleScope with repeatOnLifecycle(STARTED), so every UI write happens on Main and stops when the screen is hidden.
  4. Enforce it: @MainThread and @WorkerThread annotations checked by lint, StrictMode to flag disk and network on Main, and Dispatchers.setMain with a test dispatcher in unit tests.
  5. Note the edge: postValue coalesces, so rapid updates can drop intermediate values; use setValue or a Flow when every value matters.

Follow-up

  • What is the difference between Dispatchers.Main and Dispatchers.Main.immediate?
  • Why can collecting a Flow in lifecycleScope without repeatOnLifecycle waste resources?
  • How would you test that a ViewModel emits its state on the main thread?

Describe a scenario where you would use a volatile variable.

Medium · Concurrency and Multithreading

Approach

  1. Say what volatile guarantees: visibility (a read sees the latest write) and ordering (a write happens-before every later read of that variable, which also publishes writes made before it). It does not make compound actions atomic.
  2. Give the canonical scenario: a volatile boolean running flag written by one thread and read in a worker loop. Without volatile the loop may never see the change because the read can be hoisted or cached.
  3. Give the second scenario: double-checked locking for a lazy singleton needs the instance field to be volatile, otherwise another thread can see a non-null reference to a partially constructed object.
  4. Name where it is wrong: count++ is read-modify-write, so use AtomicInteger or synchronized. For check-then-act on a flag, use AtomicBoolean.compareAndSet.
  5. Add language details: in Kotlin it is the @Volatile annotation; volatile also makes long and double writes atomic on 32-bit JVMs.

Follow-up

  • Why is double-checked locking broken without volatile?
  • When would you choose AtomicBoolean over a volatile boolean?
  • How does Kotlin's by lazy handle thread safety by default?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

What are the main advantages of migrating a codebase from Java to Kotlin?

Medium · Technical and Domain Knowledge (Java/Kot

Approach

  1. Lead with null safety: nullability is part of the type (String vs String?), so whole classes of NullPointerException become compile errors; mention ?., ?: and why !! is a smell.
  2. Cover expressiveness that removes bugs, not just lines: data classes (equals/hashCode/copy), sealed classes with exhaustive when, extension functions, default and named arguments, and coroutines replacing callback chains.
  3. Stress full interop: Kotlin and Java compile to the same bytecode, so migration can be file by file. Name the hole: Java types arrive as platform types (String!), so add @Nullable/@NonNull to Java before converting.
  4. Give a migration plan: new code in Kotlin, convert leaf classes and tests first, review J2K output instead of trusting it, and add @JvmStatic, @JvmOverloads or @JvmField where Java callers remain.
  5. Name the costs: build time (move kapt processors to KSP where possible), team learning curve, larger APK if kotlin-stdlib is not shrunk, and mixed-language debugging during the transition.

Follow-up

  • How can a platform type still produce a NullPointerException in Kotlin code, and how would you catch it?
  • How would you measure whether the migration was worth it?
  • What breaks for Java callers when you convert a class with default arguments or companion objects?

Can you explain how Dependency Injection works and why we use it in modern applications?

Medium · Technical and Domain Knowledge (Java/Kot

Approach

  1. Define it: a class receives its dependencies instead of constructing them, so the object graph is assembled in one place. Prefer constructor injection (explicit, immutable, easy to fake); use field injection only for classes the framework instantiates, such as Activities.
  2. Give the reasons with examples: swapping a real repository for a fake in a ViewModel test, swapping implementations per build flavour, and controlling lifetimes so a shared OkHttp client is created once.
  3. Compare mechanisms: manual DI through a composition root; Dagger, which generates the graph at compile time and fails the build on a missing binding; Hilt, which adds Android scopes (@Singleton, @ActivityRetainedScoped, @ViewModelScoped, @HiltViewModel); and Koin, a runtime resolver closer to a service locator.
  4. Name the trade-offs: Dagger and Hilt cost build time and a learning curve; runtime resolvers fail at runtime; wrong scoping leaks, such as a singleton holding an Activity.
  5. Contrast with a service locator: the class pulls its dependencies from a global registry, which hides them from its signature and makes tests depend on global state.

Follow-up

  • How would you scope a dependency to one screen or one ViewModel?
  • How do you replace a binding in an instrumented test with Hilt?
  • When is manual DI the better choice than a framework?

What feature would you add to a popular music app, and how would you build the backend to support it?

Medium · Product Sense and System Design

Approach

  1. Open with the product: pick a user (for example friends listening together), the problem (no shared control of a queue) and one success metric (sessions started and their length). Scope to one feature, such as a shared listening session with a common queue.
  2. Outline the backend: a session service that creates sessions and issues join links; session state (queue, current track, position, host) in a fast store keyed by session id; a WebSocket channel per session for queue and playback events; a REST API for joins and history.
  3. Handle consistency: the server assigns a sequence number to each queue operation so clients apply them in order; playback sync uses server timestamps plus a client clock-offset estimate; reconnecting clients fetch the latest snapshot and then resume the event stream.
  4. Cover edge cases: the host disconnects (promote a guest or end the session), host and guest permissions, content not licensed in one participant's region, rate limits on queue edits, and a polling fallback when WebSockets are blocked.
  5. Close with the client: optimistic queue edits that roll back on a server rejection, and how you would A/B test the feature against the metric you picked.

Follow-up

  • How do you keep playback positions in sync across devices with different network latency?
  • What happens to the session when the host's app is killed?
  • How would you know if the feature succeeded after launch?

Design a system for a mobile app that needs to sync large amounts of offline data once a network connection is

Medium · Product Sense and System Design

Design a system for a mobile app that needs to sync large amounts of offline data once a network connection is restored.

Approach

  1. Make the local database (Room/SQLite) the source of truth: the UI reads only from it, and every user change is written locally plus to an outbox table of pending operations with a client-generated UUID, entity id, operation type and base version.
  2. Upload: a WorkManager job with a network constraint (unmetered for large media) and exponential backoff, also triggered on app foreground. Send in ordered batches per entity; for large files use chunked, resumable uploads that record the last confirmed offset; the server treats each UUID as an idempotency key.
  3. Download: delta sync with a server-issued cursor or change token, paginated, applying changes in a transaction and recording tombstones for deletes, so a resumed sync never re-downloads everything.
  4. Resolve conflicts with server versions, not device clocks: reject or merge when the base version is stale. Choose per data type: last-writer-wins for low-value fields, field-level merge for forms, or surface the conflict to the user.
  5. Cover failures: the app killed mid-batch (outbox rows stay until acknowledged), permanent server rejections (move to a dead-letter state and tell the user), schema changes to queued operations, storage and battery limits, and a visible sync status in the UI.

Follow-up

  • Two devices edit the same record offline. Walk through what each sees after both reconnect.
  • How do you keep the outbox from growing without bound if the server keeps rejecting an operation?
  • How would you test sync under flaky networks?

If users are complaining that an application feels "slow," how do you investigate and resolve the issue?

Medium · Product Sense and System Design

Approach

  1. Clarify what slow means: cold start, one screen loading, scroll jank or a specific action, and for whom (device tier, OS version, region, app version), and whether it started with a release.
  2. Measure before fixing: client data (startup time, slow and frozen frames from Android vitals or performance traces), backend latency percentiles per endpoint, and traces that join a user action to its network calls.
  3. Reproduce on a low-end device and profile: a Perfetto system trace or Android Studio CPU profiler for main-thread work, StrictMode for disk and network on Main, the network inspector for call count and payload size, and Macrobenchmark for startup and scrolling.
  4. Match causes to fixes: heavy Application.onCreate (lazy init, App Startup), main-thread I/O (move to Dispatchers.IO), N+1 API calls (batch endpoint), missing database index or cache, oversized images (resize and cache), GC churn in list binding, deep layouts.
  5. Verify and guard: compare before and after with the same benchmark, ship Baseline Profiles for startup, and add a performance regression check in CI.

Follow-up

  • The median looks fine but p99 is bad. Where do you look?
  • How do you improve perceived speed when the real latency cannot drop?
  • How would you stop the regression coming back in a later release?

How would you architect a messaging feature within an existing job-search application?

Medium · Product Sense and System Design

Approach

  1. Clarify scope: who can message whom (candidate and recruiter, 1:1 to start), attachments such as resumes, read receipts, push notifications, and moderation; tie each conversation to a job posting or application so permissions can check it.
  2. Data model: conversations, participants with a per-user read cursor, and messages with a server id, a client_msg_id for idempotent retries, sender, body, attachment reference, created_at and a per-conversation sequence number used for ordering and pagination.
  3. Delivery: online clients get messages over a WebSocket connection; offline users get FCM or APNs pushes; clients load history by paging backwards on sequence number. Partition message storage by conversation id.
  4. Integrate with the existing app: reuse its authentication and user profiles, enforce rules such as a recruiter only messaging applicants or opted-in candidates, and scan attachments.
  5. Client side: local cache of conversations, optimistic send with a pending state, retry from an outbox using the same client_msg_id, and unread counts derived from read cursors rather than recounted on every load.

Follow-up

  • How do you guarantee messages appear in the same order for both participants?
  • A recruiter has thousands of open conversations. How do you serve their inbox and unread counts efficiently?
  • How do you handle blocking and abuse reports?

Explain the concept of a deadlock and provide an example of how it might occur in Java.

Medium · Concurrency and Multithreading

Approach

  1. Define it with the four Coffman conditions: mutual exclusion, hold and wait, no preemption, and circular wait. Removing any one prevents deadlock.
  2. Write the classic example: transfer(a, b) takes synchronized(a) then synchronized(b); a concurrent transfer(b, a) takes them in reverse; each thread holds one monitor and waits forever for the other.
  3. Give the fixes: a global lock order (lock the account with the lower id first), ReentrantLock.tryLock with a timeout and back-off, smaller critical sections, never calling unknown callbacks while holding a lock, or replacing locks with ConcurrentHashMap, atomics or a single-threaded executor.
  4. Explain diagnosis: a thread dump (jstack or kill -3) reports the Java-level deadlock and the monitors each thread holds; ThreadMXBean.findDeadlockedThreads works programmatically; on Android an ANR trace shows the main thread BLOCKED on a monitor.
  5. Mention the Android-specific shape: runBlocking on the main thread waiting for work that needs Dispatchers.Main, or Future.get() on a single-threaded executor from a task already running on it.

Follow-up

  • Can a deadlock happen with only one lock or one thread pool?
  • How is a livelock different, and how would you spot one?
  • How would you test that your lock-ordering fix actually holds under load?

Suggested practice checklist

Each day covers one cluster of the reported questions, in the order the rounds are reported: language and platform first, then concurrency, then product design and behavioral. Every task produces something you can review on the last day.

Day 1 — Kotlin versus Java: language semantics and migration

  • Write a Kotlin file that shows val holding a mutable list, a custom-getter val, a data class with copy(), nullable types and an extension function; annotate each line with the Java it compiles to.
  • Answer the Java-to-Kotlin migration question aloud, covering null safety, platform types, interop annotations, build-time cost and a file-by-file plan.
  • Answer the val versus var question aloud, ending on how immutability helps thread safety.

Deliverable: An annotated Kotlin file and two recorded answers on migration and immutability.

Day 2 — Android lifecycle, memory and garbage collection

  • Draw the Activity lifecycle and mark what a configuration change and process death each destroy; note how ViewModel and SavedStateHandle cover each.
  • Write three leak examples (static Activity reference, inner-class callback, unregistered listener) and the fix for each.
  • Explain generational GC and ART's concurrent copying collector, then list allocation hot spots in onDraw and list binding with their fixes.

Deliverable: A lifecycle diagram, a leak-and-fix sheet, and a one-page GC-to-jank explanation.

Day 3 — Dependency injection and app architecture

  • Build a small screen with a repository and ViewModel using manual constructor injection, then rewire it with Hilt.
  • Write a unit test that swaps the repository for a fake, and note what changes between the manual and Hilt versions.
  • Write a comparison of Dagger/Hilt, Koin and a service locator covering failure timing, build cost and scoping.

Deliverable: A two-version sample app with one test, and a DI comparison table.

Day 4 — Java concurrency fundamentals

  • Write the two-account transfer deadlock in Java, run it until it hangs, capture a thread dump, then fix it with lock ordering and with tryLock.
  • Write a volatile stop-flag example and a broken volatile counter, then fix the counter with AtomicInteger.
  • Answer the deadlock and volatile questions aloud, including double-checked locking.

Deliverable: A deadlock repro with thread dump and two fixes, a volatile demo, and two recorded answers.

Day 5 — Asynchronous work on Android

  • Implement one fetch-and-display flow with coroutines and Flow, then with RxJava, and note the threading and cancellation differences.
  • Write a WorkManager upload job with a network constraint, backoff and unique work, and a progress observer in the UI.
  • Answer the background-tasks and main-thread questions aloud, naming every mechanism and how you would enforce it in tests.

Deliverable: A coroutines-versus-Rx comparison, a working WorkManager job, and two recorded answers.

Day 6 — Mobile system design

  • Outline the offline-sync design: outbox, resumable upload, delta download, conflict rule and failure cases.
  • Outline the messaging feature for a job-search app: data model, delivery path, permissions and client outbox.
  • Run the slow-app investigation as a written sequence from clarifying questions to verification.

Deliverable: Two design outlines and a step-by-step performance investigation checklist.

Day 7 — Product sense, behavioral stories and a full mock

  • Pick one music-app feature and present it aloud from user and metric to backend design and edge cases.
  • Write a STAR story for pushing back on a requirement, plus stories on a reversed decision and a deadline trade-off.
  • Run a mock covering one language question, one concurrency question and one design question back to back, then review the week's weakest answer.

Deliverable: A recorded product answer, three written STAR stories, and notes from the mock.

Behavioural preparation

Candidates report product-focused behavioral questions in the final stage of this loop, so your stories should show technical judgement in conversation with product: quantified constraints, alternatives you offered, collaboration with product and design, and ownership of difficult concurrency or performance problems.

  1. Describe a time you had to debug a complex race condition. What was your approach?
  2. How do you decide what features to cut when facing a strict release deadline?
  3. Tell me about a technical decision you later reversed, what changed your mind, and what the reversal cost.
  4. Describe a time you explained a technical trade-off to a non-technical stakeholder and changed the plan as a result.

Tell me about a time you pushed back on a product requirement because of technical constraints.

Medium · Product Sense and System Design

Approach

  1. Use STAR, keep the situation short, and choose a story where you made the call: a specific requirement, with a deadline or stakeholder attached.
  2. State the constraint with evidence: a number (battery drain, API rate limit, latency, sync conflicts, a platform restriction such as background execution limits), and how you measured or reproduced it rather than asserting it.
  3. Show the alternative you offered, not just a refusal: a reduced scope, a phased rollout, or a different technical route that met most of the product goal, and how you compared the options with the product manager.
  4. End with the result and the follow-through: what shipped, the measured outcome, how the relationship with product held up, and what you would change.
  5. Avoid the sinking version: framing it as beating product, a vague constraint like 'it was too complex', or a story with no alternative and no outcome.

Follow-up

  • What would you have done if the product manager overruled you?
  • How do you decide when a constraint is worth pushing back on versus absorbing?
  • Looking back, was the pushback right? What evidence tells you?

Reverse your own decision and price the reversal

Medium · reversibility · measurement · migrations

Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.

Approach

  1. State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
  2. Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
  3. Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
  4. Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.

Follow-up

  • What in that decision was irreversible, and did you know it was irreversible when you made it?
  • How did you tell the people who had already built on top of the original decision?

Ship under a deadline and bound the debt you chose

Medium · pagination · technical debt · tradeoffs

You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.

Approach

  1. Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
  2. Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
  3. Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
  4. Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.

Follow-up

  • At what page depth does the offset version breach your latency budget, given your page size and row counts?
  • What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?