Database and mobile development concept

A Deep Dive into Isar DB

February 15, 202510 min read
Share:

Our production Flutter app was grinding to a halt. What started as smooth scrolling through a few hundred records had devolved into stuttering lists and frustrated users as our dataset grew to tens of thousands of entries. We were using Hive, and while it had served us well initially, the performance bottlenecks became impossible to ignore. Eventually we reached a breaking point, as even the most menial of tasks became a really painful experience for our users. We needed a solution, and we needed it fast.

That's when we discovered Isar. The migration took sometime, but the results were worthwile. Queries that took seconds now completed in milliseconds. Complex filtering operations that made users wait became instantaneous. Our app felt new again.

In this article, I'll walk you through why we made the switch, how Isar compares to other Flutter persistence options, and the patterns we've learned from running it in production. If you're hitting performance walls with your current database solution, this might be exactly what you need.

What is Isar?

Isar is a lightning fast, NoSQL database specifically built for Flutter and Dart. Released in 2021, it's written in Rust with Dart bindings, offering native performance while maintaining a clean, idiomatic Dart API. Unlike other Flutter databases that are ports from other platforms, Isar was designed from the ground up for mobile development.

The key distinguishing features:

  • Blazing fast: Consistently outperforms SQLite, Hive, and ObjectBox in benchmarks
  • Type safe: Fully typed queries with code generation
  • Synchronous and asynchronous: Choose the right API for your use case
  • ACID compliant: Full transaction support with rollback capabilities
  • Multi isolate support: Safe concurrent access across isolates
  • Zero configuration: No boilerplate setup required

The Flutter Persistence Landscape

Before diving into Isar, let's understand the current options:

SharedPreferences

Best for: Simple key value storage

final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'john_doe');

Limitations: No complex queries, no relations, limited data types, performance degrades with size.

Hive

Best for: Fast, simple NoSQL storage

final box = await Hive.openBox('users');
box.put('user1', User(name: 'John', age: 30));

Limitations: Manual indexing, weaker query capabilities, no built in relations.

SQLite (via sqflite)

Best for: Complex relational data

final db = await openDatabase('my_db.db');
await db.insert('users', {'name': 'John', 'age': 30});

Limitations: Raw SQL strings (error prone), manual serialization, synchronous only.

Drift (formerly Moor)

Best for: Type safe SQL

// Using Drift's query builder
final users = await (select(users)
..where((u) => u.age.isBiggerOrEqualValue(18))).get();

Limitations: SQL based (steeper learning curve), more boilerplate, slower than NoSQL.

Why Isar Stands Out

Isar combines the best aspects of these solutions while avoiding their pitfalls:

Performance That Matters

Real world benchmark results (inserting 10,000 records):

  • Isar: ~150ms
  • Hive: ~280ms
  • SQLite: ~450ms
  • ObjectBox: ~200ms

Query performance (filtering 10,000 records):

  • Isar: ~8ms
  • Hive: ~45ms
  • SQLite: ~60ms

Type Safety Without Compromise

Unlike SQLite's string based queries or Hive's dynamic typing, Isar provides full type safety:

// Type safe queries - compiler catches errors
final adults = await isar.users
.filter()
.ageGreaterThan(18)
.findAll();
// Compare to SQLite:
// await db.rawQuery('SELECT * FROM users WHERE age > ?', [18]);
// (No compile time checking, easy to make mistakes)

Getting Started with Isar

Installation

dependencies:
isar: ^3.1.0
isar_flutter_libs: ^3.1.0
dev_dependencies:
isar_generator: ^3.1.0
build_runner: ^2.4.0

Defining Models

Isar uses code generation for maximum performance. Here's a complete example:

import 'package:isar/isar.dart';
part 'user.g.dart';
@collection
class User {
Id id = Isar.autoIncrement;
@Index()
late String name;
late int age;
@Index(type: IndexType.value)
late String email;
// Embedded objects
late Address? address;
// Lists
late List<String> tags;
}
@embedded
class Address {
late String street;
late String city;
late String country;
}

Opening a Database

final isar = await Isar.open([
UserSchema,
ProductSchema,
OrderSchema,
]);

Advanced Isar Features

Powerful Queries

Isar's query builder is intuitive and performant:

// Complex filtering
final results = await isar.users
.filter()
.ageGreaterThan(18)
.and()
.emailContains('@gmail.com')
.or()
.tagsElementEqualTo('premium')
.sortByName()
.limit(50)
.findAll();
// Full text search
final searchResults = await isar.users
.filter()
.nameMatches('*john*', caseSensitive: false)
.findAll();
// Aggregations
final avgAge = await isar.users
.filter()
.ageGreaterThan(0)
.ageAverage();

Links and Relations

Isar makes relations easy without foreign keys:

@collection
class Author {
Id id = Isar.autoIncrement;
late String name;
// One to many relationship
final books = IsarLinks<Book>();
}
@collection
class Book {
Id id = Isar.autoIncrement;
late String title;
// Many to one (backreference)
final author = IsarLink<Author>();
}
// Using relations
final author = Author()..name = 'John Doe';
final book = Book()..title = 'Flutter Guide';
await isar.writeTxn(() async {
await isar.authors.put(author);
author.books.add(book);
await author.books.save();
});
// Query across relations
final authorsWithBooks = await isar.authors
.filter()
.books((q) => q.titleContains('Flutter'))
.findAll();

Transactions

ACID compliant transactions ensure data integrity:

// Write transaction
await isar.writeTxn(() async {
final user = User()
..name = 'John'
..age = 30;
await isar.users.put(user);
// If any operation fails, entire transaction rolls back
if (user.age < 18) {
throw Exception('User must be adult');
}
await isar.settings.put(Settings()..lastUser = user.id);
});
// Read transactions (faster, no locking)
final users = await isar.txn(() async {
return await isar.users.where().findAll();
});

Watchers and Reactive Queries

Isar provides real time updates:

// Watch a single object
final userStream = isar.users.watchObject(userId);
userStream.listen((user) {
print('User updated: ${user?.name}');
});
// Watch query results
final adultsStream = isar.users
.filter()
.ageGreaterThan(18)
.watch(fireImmediately: true);
// Use in StreamBuilder
StreamBuilder<List<User>>(
stream: adultsStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final user = snapshot.data![index];
return ListTile(title: Text(user.name));
},
);
},
);

Indexes for Performance

Strategic indexing dramatically improves query speed:

@collection
class Product {
Id id = Isar.autoIncrement;
// Simple index (faster equality checks)
@Index()
late String sku;
// Composite index (queries on category + price)
@Index(composite: [CompositeIndex('price')])
late String category;
late double price;
// Unique index (enforces uniqueness)
@Index(unique: true)
late String barcode;
// Hash index (fastest for equality, no range queries)
@Index(type: IndexType.hash)
late String internalCode;
}
// Queries that use indexes
final product = await isar.products
.filter()
.skuEqualTo('ABC123') // Uses sku index
.findFirst();
final electronics = await isar.products
.filter()
.categoryEqualTo('Electronics') // Uses composite index
.and()
.priceLessThan(1000)
.findAll();

Migration Strategies

Isar handles schema changes gracefully:

// Version 1
@collection
class User {
Id id = Isar.autoIncrement;
late String name;
}
// Version 2: Adding field (automatic)
@collection
class User {
Id id = Isar.autoIncrement;
late String name;
String? email; // New nullable field
}
// Version 3: Data migration needed
Future<void> migrateToV3(Isar isar) async {
final oldUsers = await isar.users.where().findAll();
await isar.writeTxn(() async {
for (var user in oldUsers) {
// Migrate data
user.email ??= '${user.name.toLowerCase()}@example.com';
await isar.users.put(user);
}
});
}

Performance Optimization Patterns

Batch Operations

// Inefficient: Multiple transactions
for (var user in users) {
await isar.writeTxn(() async {
await isar.users.put(user);
});
}
// Efficient: Single transaction
await isar.writeTxn(() async {
await isar.users.putAll(users);
});

Lazy Loading

// Load only what you need
final userIds = await isar.users
.where()
.idProperty()
.findAll();
// Load full objects on demand
final user = await isar.users.get(userIds.first);

Using Synchronous API

When performance is critical and you're not on the main isolate:

// Async (typical usage)
final users = await isar.users.where().findAll();
// Sync (faster, but blocks thread)
final users = isar.users.where().findAllSync();

Real World Use Cases

Offline First App with Sync

class DataRepository {
final Isar isar;
DataRepository(this.isar);
// Save data locally
Future<void> saveArticle(Article article) async {
await isar.writeTxn(() async {
article.syncStatus = SyncStatus.pending;
await isar.articles.put(article);
});
_syncToServer();
}
// Sync pending changes
Future<void> _syncToServer() async {
final pending = await isar.articles
.filter()
.syncStatusEqualTo(SyncStatus.pending)
.findAll();
for (var article in pending) {
try {
await api.syncArticle(article);
await isar.writeTxn(() async {
article.syncStatus = SyncStatus.synced;
await isar.articles.put(article);
});
} catch (e) {
// Handle sync errors
}
}
}
}

Implementing Full Text Search

@collection
class Note {
Id id = Isar.autoIncrement;
@Index(type: IndexType.value, caseSensitive: false)
late String title;
@Index(type: IndexType.value, caseSensitive: false)
late String content;
late DateTime createdAt;
}
// Search implementation
Future<List<Note>> searchNotes(String query) async {
final words = query.toLowerCase().split(' ');
return await isar.notes
.filter()
.anyOf(
words,
(q, word) => q
.titleContains(word, caseSensitive: false)
.or()
.contentContains(word, caseSensitive: false),
)
.sortByCreatedAtDesc()
.findAll();
}

Isar vs The Competition

When to Use Isar

  • High performance requirements (thousands of records)
  • Complex queries needed
  • Type safety is important
  • Multi isolate access required
  • NoSQL flexibility preferred

When to Use SQLite

  • Team has strong SQL expertise
  • Need raw SQL query control
  • Existing SQL schema to migrate
  • Simpler, well understood technology

When to Use Hive

  • Extremely simple use case
  • Minimal dependencies preferred
  • Very small data sets
  • Learning curve is primary concern

When to Use Drift

  • Strong preference for SQL
  • Need migration tooling
  • Complex joins and relations
  • Type safety with SQL

Common Pitfalls and Solutions

Forgetting to Run Code Generation

# Always run after model changes
flutter pub run build_runner build

Not Using Indexes

// Slow: No index
@collection
class User {
late String email;
}
// Fast: With index
@collection
class User {
@Index()
late String email;
}

Keeping Database Open

// Open once, use everywhere
class Database {
static Isar? _instance;
static Future<Isar> get instance async {
if (_instance != null) return _instance!;
_instance = await Isar.open([UserSchema]);
return _instance!;
}
}

Testing Strategies

void main() {
late Isar isar;
setUp(() async {
// Use in-memory database for tests
isar = await Isar.openInMemory([UserSchema]);
});
tearDown(() async {
await isar.close(deleteFromDisk: true);
});
test('should save and retrieve user', () async {
final user = User()
..name = 'John'
..age = 30;
await isar.writeTxn(() async {
await isar.users.put(user);
});
final retrieved = await isar.users.get(user.id);
expect(retrieved?.name, 'John');
});
}

Conclusion

Isar represents a significant leap forward in Flutter data persistence. Its combination of raw performance, type safety, and developer friendly API makes it an excellent choice for most Flutter applications that need local storage beyond simple key value pairs.

The decision matrix is straightforward:

  • Simple key value needs: SharedPreferences
  • Small datasets with simple queries: Hive
  • SQL expertise and complex joins: Drift
  • High performance NoSQL with complex queries: Isar

After building several production apps with Isar, I can confidently say it delivers on its promises. The performance gains are real, the API is intuitive, and the type safety catches bugs before they reach production.

If you're starting a new Flutter project or considering migrating from another database solution, give Isar a serious look. The investment in learning its patterns pays dividends in performance and maintainability.


Further Resources:

Try it yourself: Clone the Isar samples repo to see real world implementations and best practices.

Found this helpful? Share it with others!

Share: