Skip to main content

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)