Architecture Overview
DB plugin is a flexible, extensible database abstraction layer that provides a unified interface for working with multiple database backends. It uses a data mapper pattern to separate domain objects from persistence logic, enabling powerful features like multi-database routing, caching, and real-time replication.
Core Components
Database & AbstractDatabase
The Database interface is the central abstraction that defines all database operations. It provides methods for:
- Reading: Query execution, counting, pagination
- Writing: Saving, deleting, indexing
- Transactions: Begin/commit/rollback operations
- Configuration: Connection management, naming, utilities
Multiple database implementations can be registered and accessed by name, allowing your application to work with different backends simultaneously.
State
State is the core data container that holds an object's field values. Think of it as a flexible map structure that:
- Stores raw field values (primitives, collections, references)
- Manages resolved references (lazy-loaded related objects)
- Tracks validation errors
- Maintains object lifecycle state (new, saved, deleted)
- Provides path-based field access (
author/name)
The State layer separates data storage from business logic, enabling powerful features like modifications and polymorphism.
Record & Recordable
Recordable is the minimal interface for database-backed objects:
public interface Recordable {
State getState();
void setState(State state);
String getLabel();
<T> T as(Class<T> targetClass);
}
Record is the abstract base class that most domain objects extend. It provides:
- Lifecycle hooks (
beforeSave,afterSave,beforeDelete,afterDelete) - Validation hooks (
onValidate,afterValidate) - Convenience methods for save/delete operations
- Integration with the State layer
Query
The Query class provides a fluent, type-safe API for building and executing queries. It's inspired by Apple's Cocoa Predicates and LINQ:
List<Article> articles = Query.from(Article.class)
.where("author/name = ?", "John Doe")
.and("publishDate > ?", cutoffDate)
.sortDescending("publishDate")
.select(0, 10);
Queries support predicates, sorting, pagination, grouping, and various execution modes (streaming, counting, etc.).
ObjectType
ObjectType represents metadata about your domain types:
- Field definitions (name, type, validation rules)
- Index configurations
- Type hierarchy and relationships
- Groups for polymorphic queries
- Source database mapping
Types are discovered and initialized at startup through the DatabaseEnvironment.
ObjectField
ObjectField describes individual fields within a type:
- Field name and Java type
- Indexing configuration
- Validation rules and annotations
- Embedded/denormalized flags
- Unique constraints
Architecture Patterns
Data Mapper Pattern
DB plugin uses the data mapper pattern to keep domain objects independent of persistence logic:
This separation allows:
- Domain objects to focus on business logic
- Transparent switching between database implementations
- Testing without database dependencies
- Complex persistence strategies (caching, replication, federation)
Composition over Inheritance (Modifications)
Modifications enable aspect-oriented composition without complex inheritance hierarchies:
Multiple objects can link to the same State, each providing different views and behaviors:
Article article = state.as(Article.class);
SEOData seo = state.as(SEOData.class); // Modification
SocialData social = state.as(SocialData.class); // Another modification
This pattern allows:
- Cross-cutting concerns to be cleanly separated
- Dynamic behavior composition at runtime
- Multiple aspects on the same object without inheritance conflicts
Lazy Loading (Reference Resolution)
References to other objects are stored as lightweight stubs and resolved on-demand:
Benefits:
- Reduces initial query overhead
- Batch resolution for N+1 query prevention
- Configurable resolution depth
- Support for reference-only queries
Transaction Management
Transactions use a depth-based nesting model with validation phases:
Features:
- Nested transaction support
- Write buffering and batching
- Pre-write validation
- Trigger firing after commit
- Automatic retry on recoverable errors
Database Implementations
DB plugin supports multiple backend implementations:
AbstractSqlDatabase (platform-sql)
Stores objects in relational databases (MySQL, PostgreSQL, Oracle, SQL Server, etc.):
- Automatic schema management from ObjectType metadata
- Optimized for transactional workloads
- Strong consistency guarantees
- Full transaction support
SolrDatabase (platform-solr)
Integrates with Apache Solr for full-text search:
- Optimized for search and faceting
- Near real-time indexing
- Relevance scoring and boosting
- Eventually consistent
CachingDatabase
Wraps another database with caching layers:
- Query result caching
- Reference resolution caching
- Configurable TTLs and eviction
- Cache invalidation on writes
AggregateDatabase
Combines multiple databases for redundancy:
- Writes go to all databases
- Reads from first available
- Automatic failover
- Consistency checking
Data Flow
Read Operation Flow
Write Operation Flow
Multi-Database Configuration
Applications can work with multiple databases simultaneously:
Access databases by name:
Database defaultDb = Database.Static.getDefault();
Database searchDb = Database.Static.getInstance("search");
// Or use Query.using()
Query.from(Article.class)
.using(searchDb)
.where("_any matches ?", "search term")
.selectAll();
Key Design Decisions
No Joins, Use Subqueries
DB plugin deliberately avoids joins in favor of subqueries and reference resolution:
// Instead of a join
Query.from(Article.class)
.where("author = ?", Query.from(Author.class)
.where("name = ?", "John Doe"))
.selectAll();
This approach:
- Works consistently across all database types (SQL, NoSQL, search engines)
- Enables better caching strategies
- Simplifies query optimization
- Allows transparent cross-database queries
String-Based Predicates
Queries use string-based predicates instead of method chains:
// DB plugin style
.where("publishDate > ? and author/name = ?", date, name)
// vs. alternative method-chain style
.where(field("publishDate").gt(date).and(field("author", "name").eq(name)))
Benefits:
- More concise and readable
- Easier to generate dynamically
- Familiar to developers from SQL, LINQ, Cocoa
- Supports complex nested expressions naturally
Field-Level Indexing Required
Fields must be explicitly marked as indexed to be queried:
@Recordable.Indexed
private String title;
This design:
- Makes performance characteristics explicit
- Prevents accidental full table scans
- Enables schema optimization
- Works well with non-relational databases
Validation Before Write
All validation occurs in memory before any database writes:
beginWrites();
save(object1); // Buffered, not written
save(object2); // Buffered, not written
commitWrites(); // Validates all, then writes all or none
Benefits:
- Fast failure without database roundtrips
- Atomic batch operations
- Consistent error handling
- Better error messages (all errors at once)