When you're building an Android app, you need to store data locally. Sure, you could use SharedPreferences for simple key-value pairs, but when your data gets complex — multiple objects, relationships, queries — you need a real database.
That's where Room comes in.
Room is Android's recommended SQLite abstraction layer. It's built on top of SQLite, which means you get a battle-tested, lightweight database that ships with every Android device. But Room adds a critical layer on top: compile-time SQL verification and Kotlin coroutines support.
In this article, I'll walk you through Room's three core components and show you the exact code patterns that AI tools like Claude Code generate when building production Android apps.
The Three Pillars of Room
Room architecture is built on three interconnected pieces:
Entity — Your data model. Describes the shape of your data.
DAO — Data Access Object. The methods you use to read/write data.
Database — The Room database holder. Creates the DAOs and manages the database file.
Let's build a real example: a Note-Taking App.
1. Entity: Defining Your Data
An Entity is a Kotlin data class that represents a table in your SQLite database. Room reads the annotations and generates the SQL for you.
Here's an example Note entity:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val isPinned: Boolean = false
)
Let's break this down:
@entity: Tells Room this class maps to a database table called "notes"
@PrimaryKey(autoGenerate = true): Theidfield is the primary key. SQLite auto-increments it when you insert a new note.
val id: Int = 0: The default value of 0 signals "this is a new record, assign the next ID"
Long timestamps: This is crucial. Store timestamps asLong(Unix epoch milliseconds), not as Strings. It's faster in queries, avoids timezone bugs, and serializes cleanly.
Why not use Date or Instant? Because SQLite doesn't have a native date type. Storing as Long is the idiomatic approach in Android.
2. DAO: How You Access the Data
The DAO is where you define the methods to read, create, update, and delete your notes. Room generates the SQL at compile time.
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE isPinned = 1 ORDER BY updatedAt DESC")
fun getPinnedNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Int): Note?
@Insert
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
@Query("DELETE FROM notes WHERE id = :noteId")
suspend fun deleteNoteById(noteId: Int)
}
Notice three critical patterns here:
Pattern 1: Flow for Reactive Queries
fun getAllNotes(): Flow<List<Note>>
Flow is Kotlin's reactive stream. When the database changes, Flow automatically emits the new data. Your UI observes this Flow and updates automatically — no callbacks, no manual refresh. This is how modern Android apps work.
Pattern 2: Suspend Functions for Async
suspend fun getNoteById(noteId: Int): Note?
suspend fun insertNote(note: Note): Long
Suspend functions run on a background dispatcher. When you call insertNote() from a ViewModel, Room handles the threading automatically. You don't need to manually create coroutines or Thread objects.
Pattern 3: Separation of Concerns
Each DAO method does one thing:
getAllNotes()— get all notes, reactive
getPinnedNotes()— get pinned notes, reactive
getNoteById()— get a single note, one-time query
insertNote()— create a new note
updateNote()— modify an existing note
deleteNote()— delete by object
deleteNoteById()— delete by ID
This keeps your database layer clean and testable.
3. Database: The Room Holder
The Database class is where you register your Entities and DAOs. You create it once per app and keep a reference to it in your dependency injection container (or a singleton).
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [Note::class], version = 1)
abstract class NoteDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: NoteDatabase? = null
fun getDatabase(context: Context): NoteDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Key points:
@database: Declares this as a Room database with theNoteentity, version 1
abstract fun noteDao(): Room generates the implementation automatically
@Volatile INSTANCE: Thread-safe singleton pattern. Only one database connection per app.
synchronized(this): Prevents race conditions if multiple threads try to create the database simultaneously
How They Work Together
Here's the complete flow:
┌─────────────────────┐
│ Your UI (Screen) │ Observes Flow, calls ViewModel
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ ViewModel │ Uses Repository, launches coroutines
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Repository │ Calls DAO methods
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ NoteDao (DAO) │ Translates to SQL
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ NoteDatabase │ Manages SQLite connection
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ SQLite File │ Stored in app's private data directory
└─────────────────────┘
A ViewModel using this database looks like this:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class NoteViewModel(private val noteDao: NoteDao) : ViewModel() {
val notes = noteDao.getAllNotes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun createNote(title: String, content: String) {
viewModelScope.launch {
noteDao.insertNote(Note(title = title, content = content))
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
noteDao.deleteNote(note)
}
}
}
The notes property is a StateFlow<List<Note>>. The UI observes it and automatically updates whenever the database changes. No manual refresh needed.
Why This Matters: Privacy & Control
Room stores data in a private SQLite database file. By default, this file is stored in the app's private data directory (/data/data/com.example.noteapp/databases/note_database).
This means:
No INTERNET permission needed — Your app doesn't transmit data to a server
No cloud dependency — Works offline, works without network
User has control — The data stays on the device unless the user explicitly chooses to sync it
GDPR-compliant — No third-party servers involved in data storage
Many apps add INTERNET permission "just in case" without thinking. But if you're using Room for local storage, you don't need it. That's a privacy win.
The Complete Picture
Room is the standard solution for Android data persistence because it:
Integrates with Kotlin — Suspend functions, Flow, coroutines built in
Compile-time safety — Invalid SQL is caught at build time, not runtime
Minimal boilerplate — Annotations handle 90% of the work
Production-ready — Used in billions of apps
Privacy-first — Local storage, no network required
AI tools like Claude Code consistently generate Room databases using these exact patterns. The architecture is correct, the threading model is sound, and the code is maintainable from day one.
Next Steps
All 8 of my AI-generated Android app templates use Room with this exact pattern. They include:
Simple utilities — Unit Converter, Countdown Timer (single entity, basic queries)
Data-rich apps — Budget Manager, Task Manager with priorities (relationships, complex queries)
Real-world patterns — Repository layer, ViewModel state management, Flow integration
Check them out on Gumroad. Each template is a complete, working project you can customize for your own app.
Have you built with Room before? What's your approach to database migrations or multi-entity relationships? Drop a comment — I'd love to see what you're working on.
SOCIAL SHARE CARD GENERATOR