Why I Keep My Database Layer Boring
When building an application, it's easy to get excited about the visible parts.
A new Compose screen.
A better API.
A new feature.
A cleaner dashboard.
The database usually gets less attention.
But while working on FinLedger, I've found that the database layer is one of the places where boring engineering is actually a good thing.
For a finance application, I don't want the database to be clever.
I want it to be predictable.
Start with the data model
Before writing queries, I need to understand what I'm actually storing.
For example, a transaction isn't just:
amount
`
It can have relationships with other parts of the application:
text
Transaction
├── amount
├── date
├── type
├── category/tag
├── person
└── other metadata
Those relationships matter.
If the data model is unclear, the application code eventually becomes full of special cases trying to compensate for it.
Keep persistence responsibilities clear
A useful separation looks like:
text
UI
↓
ViewModel
↓
Repository
↓
DAO
↓
Room
↓
SQLite
The UI shouldn't need to know SQL.
The DAO shouldn't decide business rules.
The repository shouldn't become a second UI layer.
Each component should have a reasonably narrow responsibility.
For example, a DAO might answer:
kotlin
@Query("SELECT * FROM transactions ORDER BY date DESC")
fun observeTransactions(): Flow<List<Transaction>>
Its job is retrieving data.
It shouldn't also decide how the dashboard calculates financial insights.
Database constraints are useful
It's tempting to rely entirely on application code for validation.
But the database can enforce important assumptions too.
For example:
text
amount cannot be NULL
required relationship must exist
identifier must be unique
Why duplicate validation?
Because different parts of an application can eventually write data.
A database constraint provides a final line of defense.
Application validation improves the user experience.
Database constraints protect the integrity of stored data.
They're solving different problems.
Don't make every query generic
Another trap is trying to build a universal repository that can do everything.
Something like:
text
save()
update()
delete()
find()
query()
can look clean at first.
But eventually the repository becomes an abstraction that hides what the application is actually doing.
A specific operation can sometimes be clearer:
text
getTransactionsForMonth()
getTransactionsForPerson()
getTransactionsByType()
The database layer should make important data access patterns understandable.
Abstraction is useful when it removes meaningful complexity.
If it only hides a simple query behind five interfaces, it may be making the code harder to understand.
Observe data instead of manually refreshing everything
One thing I like about using Room with Kotlin is the ability to work with observable data.
Conceptually:
text
Database changes
↓
Flow emits new data
↓
ViewModel receives state
↓
Compose recomposes
That is cleaner than having every screen manually ask:
"Did the database change?"
The data source can become the trigger for updating the UI.
Performance still matters
A database can work correctly and still become a performance problem.
For example, fetching a huge dataset when the screen only needs the latest records is wasteful.
Instead of:
text
Load everything
↓
Filter in application
↓
Display 20 records
you often want:
text
Query only what you need
↓
Return relevant records
↓
Display them
The database is good at filtering data.
Use it.
The boring database principle
The more important the data, the less interested I am in clever persistence logic.
For a financial application, I want:
text
Predictable schema
Clear relationships
Explicit queries
Useful constraints
Observable data
Simple migrations
Not:
text
Magic abstraction
Hidden side effects
Queries nobody understands
Business rules scattered across DAOs
The database should be something I can inspect and reason about when something goes wrong.
The bigger lesson
I've started thinking about database code differently.
The goal isn't to write the most sophisticated persistence layer.
The goal is to make it hard to accidentally corrupt the application's data model.
That's especially important for software where the data represents something users care about.
Fancy architecture is useful when it solves a real problem.
But for the foundation of an application, boring is often a feature.
**Make the database predictable.
Make the queries understandable.
Make the data difficult to corrupt.
Then build the interesting features on top of it.**
Top comments (0)