AIDL & Inter-Process Communication (IPC)Android study notes — concepts, diagrams, code, and interview preparation.

AIDL & Inter-Process Communication (IPC)

Android study notes — concepts, diagrams, code, and interview prep

How to use this document
- Seven sections: What → Why → Comparison → Diagrams → Code → Common Mistakes → Interview Q&A.
- Code samples are Kotlin, based on a two-process calculator service example used throughout.
- Diagrams show the real Binder call path and the threading model — the two things interviewers probe hardest.

1. What is AIDL / IPC?

Android runs every app in its own Linux process, sandboxed and isolated for security and stability. That isolation is great for safety, but it means two processes cannot simply share memory or call each other's functions directly — there is no shared address space.

Inter-Process Communication (IPC) is the general term for any mechanism that lets two separate processes exchange data. On Android, the underlying transport for almost all IPC is the Binder driver, a piece of the Linux kernel written specifically for Android.

Where AIDL fits in

AIDL (Android Interface Definition Language) is a tool, not a runtime mechanism by itself. You write a .aidl file describing a set of methods; the Android build tools generate Java/Kotlin code (a Stub and a Proxy) that handles the Binder plumbing for you — marshaling arguments into a Parcel, sending them across, and unmarshaling the result.

  • AIDL = the interface-definition language + code generator.
  • Binder = the actual kernel-level transport AIDL code rides on.
  • IPC = the umbrella concept; AIDL/Binder is Android's flagship implementation of it.

You reach for AIDL specifically when you need a bound service that (a) lives in a different process, and (b) must support multiple concurrent clients calling methods with real return values — not just fire-and-forget messages.


2. Why do we need it?

A few concrete situations force you out of "just call the method" and into IPC:

  • Multi-process apps — you've split your app across processes (e.g. android:process=":remote") for memory isolation, to isolate a crash-prone SDK, or to keep a background component alive independently of the UI process.
  • Cross-app communication — a separate app (a different APK entirely) needs to call into a service you expose — e.g. a system service, a companion app, or a plugin architecture.
  • System services — almost everything you call on Context (LocationManager, NotificationManager, ActivityManager) is itself an AIDL-generated interface talking to system_server over Binder.
  • Concurrent, synchronous calls — multiple clients need to call methods and get typed return values back, with the framework handling threading and queuing — this is where AIDL earns its complexity over simpler alternatives.
Rule of thumb
If your client and service run in the same process, skip AIDL entirely — a plain bound Service with a Binder subclass (local binding) is simpler and faster. Reach for AIDL only when a real process boundary is involved.

3. Comparison: AIDL vs. the alternatives

AIDL is the heaviest tool in the IPC toolbox. Before reaching for it, check whether a lighter option already solves your problem.

MechanismCross-process?ConcurrencyComplexityBest for
Local Binder (same process)NoSame thread as callerLowBound service used only within your own app/process
MessengerYesSerial — one handler thread, messages queuedLow–MediumSimple async message passing, no need for concurrent calls
AIDLYesConcurrent — thread pool on service sideHighMultiple clients, synchronous calls with return values
ContentProviderYesFramework-managedMediumStructured, table-like data sharing (CRUD) across apps
Broadcast / IntentYesAsync, fire-and-forgetLowOne-to-many event notification, no return value needed
Messenger vs AIDLYesMessenger = serial, AIDL = parallelMessenger simplerChoose Messenger if you don't need concurrent processing

4. Diagrams

4.1 The Binder call path

Every AIDL method call follows the same round trip, whether it's your app or the Android framework making the call:

graph LR
    subgraph ProcessA["Process A — Client App"]
        Activity["Activity / ViewModel<br/>(client code)"]
        Proxy["IMyService.Stub.Proxy<br/>(auto-generated)"]
        Activity -->|"calls method()"| Proxy
    end

    subgraph Kernel["Kernel Space"]
        Binder["Binder Driver<br/>(/dev/binder)"]
    end

    subgraph ProcessB["Process B — Service App"]
        Stub["IMyService.Stub<br/>(onTransact)"]
        Impl["Service Implementation<br/>(your real logic)"]
        Stub -->|"dispatches call"| Impl
    end

    Proxy -->|"1. transact()<br/>Parcel written"| Binder
    Binder -->|"2. onTransact()"| Stub
    Impl -.->|"3. returns result"| Stub
    Stub -->|"4. reply Parcel"| Binder
    Binder -->|"5. result delivered"| Proxy

    style Binder fill:#4A5568,stroke:#2D3748,color:#fff
    style Proxy fill:#63B3ED,stroke:#2C5282,color:#fff
    style Activity fill:#3182CE,stroke:#2C5282,color:#fff
    style Stub fill:#38A169,stroke:#276749,color:#fff
    style Impl fill:#2F855A,stroke:#276749,color:#fff

Figure 1 — A client calls a method; the Stub.Proxy marshals it into a Parcel, the Binder driver moves it across the process boundary, and the Stub unmarshals and dispatches it to your real implementation.

4.2 The threading model

This is the single most commonly misunderstood part of AIDL, and a favorite interview trap:

flowchart TB
    ClientMain["Client: Main / UI Thread"]
    ClientCall["proxy.method()<br/>BLOCKS caller until reply arrives"]
    ServiceMain["Service: Main / UI Thread<br/>(NOT used for AIDL calls)"]
    Thread1["Binder Thread #1<br/>(from pool, default max 16)"]
    Thread2["Binder Thread #2<br/>(concurrent)"]
    Note["Your AIDL method body runs on a Binder pool thread,<br/>NEVER on the service's main thread.<br/>You must add your own synchronization for shared state."]

    ClientMain -->|"must call off UI thread!"| ClientCall
    ClientCall -->|"call A"| Thread1
    ClientCall -->|"call B (concurrent)"| Thread2
    Thread1 -.-> Note
    Thread2 -.-> Note

    style ClientMain fill:#3182CE,stroke:#2C5282,color:#fff
    style ClientCall fill:#63B3ED,stroke:#2C5282,color:#fff
    style ServiceMain fill:#A0AEC0,stroke:#4A5568,color:#fff
    style Thread1 fill:#38A169,stroke:#276749,color:#fff
    style Thread2 fill:#38A169,stroke:#276749,color:#fff
    style Note fill:#FEFCBF,stroke:#975A16,color:#744210

Figure 2 — AIDL method bodies execute on a Binder thread pool on the service side (default max 16 threads), never on the service's main thread. Calls from the client block the calling thread until a reply arrives, so clients must call from a background thread too.


5. Code walkthrough

A minimal two-process example: a CalculatorService running in a :remote process, and a ClientActivity in the main process that binds to it.

5.1 Define the interface

// IMyCalculator.aidl
package com.example.ipcdemo;

interface IMyCalculator {
    int add(int a, int b);
    String getServiceInfo();
}

IMyCalculator.aidl — placed under src/main/aidl/, mirrored on both client and service sides (or shared via a library module).

5.2 Implement the service

class CalculatorService : Service() {

    private val binder = object : IMyCalculator.Stub() {
        override fun add(a: Int, b: Int): Int {
            // Runs on a Binder pool thread, NOT the main thread
            return a + b
        }

        override fun getServiceInfo(): String {
            return "Running in process: ${Process.myPid()}"
        }
    }

    override fun onBind(intent: Intent): IBinder = binder
}

CalculatorService.kt — the generated IMyCalculator.Stub is an abstract Binder; you implement its methods with your real logic.

5.3 Force a separate process

<service
    android:name=".CalculatorService"
    android:process=":remote"
    android:exported="false" />

AndroidManifest.xml — the leading colon in android:process=":remote" creates a private process local to your app; omitting the colon creates a shared global process.

5.4 Bind and call from the client

class ClientActivity : AppCompatActivity() {

    private var calculator: IMyCalculator? = null

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            calculator = IMyCalculator.Stub.asInterface(service)
        }
        override fun onServiceDisconnected(name: ComponentName) {
            calculator = null   // service crashed or process died
        }
    }

    override fun onStart() {
        super.onStart()
        Intent(this, CalculatorService::class.java).also { intent ->
            bindService(intent, connection, Context.BIND_AUTO_CREATE)
        }
    }

    fun callAdd() {
        lifecycleScope.launch(Dispatchers.IO) {   // never call on main thread
            try {
                val result = calculator?.add(4, 5)
                withContext(Dispatchers.Main) { showResult(result) }
            } catch (e: RemoteException) {
                // service process died mid-call — handle gracefully
            }
        }
    }

    override fun onStop() {
        super.onStop()
        unbindService(connection)   // avoid leaking the ServiceConnection
    }
}

ClientActivity.kt — note the call happens on Dispatchers.IO (never main thread), and RemoteException is handled in case the service process dies mid-call.


6. Common mistakes

1. Calling AIDL methods on the main thread

A Binder call blocks the calling thread until the reply arrives. Do this on the UI thread and you get ANRs the moment the service is slow or the process is starting up. Always call from a background thread/coroutine.

2. Ignoring RemoteException

The remote process can die, be killed by the OOM-killer, or crash mid-call. Every AIDL method call can throw RemoteException — swallowing it silently (or not catching it at all) causes hard crashes that are painful to reproduce.

3. Forgetting unbindService()

Not unbinding in onStop()/onDestroy() leaks the ServiceConnection and can keep the remote service process alive indefinitely, wasting memory and battery.

4. Assuming thread-safety on the service side

Because AIDL calls run on a pool of Binder threads, two clients calling concurrently can execute your method body at the same time on different threads. Shared mutable state needs explicit synchronization — AIDL gives you concurrency for free, but not thread safety.

5. Passing non-Parcelable custom types

Every custom type crossing the AIDL boundary must implement Parcelable (or be one of AIDL's built-in supported types). Forgetting this fails at compile time for the .aidl file, or at runtime with a marshaling exception if done incorrectly.

6. Overusing oneway or forgetting it entirely

oneway makes a call non-blocking / fire-and-forget (no return value possible). Forgetting it on a callback interface can cause the calling side — often the service, calling back into the client — to block unexpectedly. Conversely, marking something oneway when you need the return value silently breaks your logic.


7. Interview Q&A

Flash-card style — cover the answer column and quiz yourself.

#QuestionAnswer
Q1What is the Binder driver and where does it live?A kernel-level driver (/dev/binder) that Android added to Linux specifically to support fast, secure IPC. It lives in kernel space and mediates data transfer between process address spaces.
Q2What do the generated Stub and Proxy classes do?Stub is an abstract Binder implementation on the service side that receives transactions (onTransact) and dispatches to your real method. Proxy is the client-side object implementing the same interface; it marshals calls into a Parcel and invokes transact() on the Binder.
Q3On which thread does an AIDL method execute on the service side?On a thread from the Binder thread pool (default max 16 threads per process), never on the service's main/UI thread — even if the service itself does no explicit threading.
Q4Is an AIDL call synchronous or asynchronous by default?Synchronous — the calling thread blocks until the reply Parcel comes back, unless the method is marked oneway.
Q5What does the oneway keyword do?Marks a method as non-blocking / fire-and-forget: the caller does not wait for the method to finish and cannot receive a return value. Commonly used for callback interfaces to avoid deadlocks.
Q6Why must AIDL calls be made off the main thread on the client?Because the call blocks until a reply arrives; if the service is slow, busy, or still starting, blocking the UI thread causes an ANR.
Q7What happens if the service process dies mid-call?The pending call throws a RemoteException on the client side, and onServiceDisconnected() is eventually invoked on the ServiceConnection so the client can clean up and potentially rebind.
Q8How do you pass a custom object through AIDL?It must implement Parcelable (writeToParcel/CREATOR), and you declare it in a matching .aidl file so the AIDL compiler knows how to (un)marshal it.
Q9What's the difference between AIDL and a local Binder (same-process) service?A local Binder just returns this from onBind() and the client casts it directly — real method calls, same thread, no marshaling. AIDL is required only when the client is in a different process, since it generates the marshaling/Binder-transaction code that crossing processes requires.
Q10When would you choose Messenger over AIDL?When you need simple, serial (one-at-a-time) cross-process messaging without concurrent access, and you don't need synchronous return values — Messenger is far less boilerplate than a full AIDL interface.
Q11What is a common cause of a TransactionTooLargeException?Trying to pass very large payloads (e.g. big Bitmaps or large lists) through a single Binder transaction — the Binder transaction buffer is limited (historically ~1MB shared across the whole process). Large data should go through a file, shared memory (e.g. ParcelFileDescriptor), or be chunked.
Q12How does android:process=":remote" differ from a process name without a colon?A leading colon creates a private process scoped to your app (e.g. com.example.app:remote); no colon creates or joins a global, potentially shared process, which requires matching signatures/sharedUserId considerations for other apps to join it.

Quick-reference cheat sheet
- Same process → local Binder. Cross-process, simple async → Messenger. Cross-process, concurrent + typed returns → AIDL.
- AIDL method body runs on a Binder pool thread — add your own synchronization.
- Client calls block the caller — always invoke off the main thread.
- Always handle RemoteException; always unbindService() when done.
- Custom types crossing the boundary must be Parcelable.