Capacitor Data Modeling Language (DML)
Version 1.0
Introduction
Capacitor is a high-level, declarative Data Modeling Language (DML) designed to decouple data intent from physical storage. It addresses the fundamental "Impedance Mismatch" between application-level object modeling and database-level storage mechanics.
Built with an AI-first philosophy, Capacitor's syntax is optimized for generation by LLM coding agents while remaining perfectly human-readable and writable. The language uses clear, unambiguous structures that both humans and AI can easily understand, making it ideal for modern development workflows where AI assists in code generation.
Capacitor allows architects to model data based on Access Patterns and Semantic Meaning rather than the specific constraints of a database engine. By defining the "What" and "Why" of data, Capacitor can be "woven" into any target database—Relational (SQL), Document (NoSQL), or Key-Value store—automatically generating the optimal physical schemas and access code for each.
The Generator Philosophy
Capacitor follows the principle of universal specification with target-specific implementation. The language specification defines the complete semantic model without database constraints. Generators handle the reality of each database through:
- Native implementations where the database supports the feature
- Shim implementations where application code provides the functionality
- Generation-time warnings when features require shims
- Generation-time errors when features cannot be implemented
This approach ensures your data model remains portable and future-proof while giving you full transparency about what happens under the hood.
Language Philosophy
Capacitor is built on five core principles:
AI-First Design
Capacitor is specifically designed to be easily generated by LLM coding agents while remaining perfectly human-readable and writable. The syntax prioritizes:
- Unambiguous structure that LLMs can reliably produce without hallucination
- Clear semantic intent that reduces AI errors and improves code quality
- Self-documenting patterns that are obvious to both humans and AI
- Consistent formatting that makes code generation predictable and maintainable
This AI-first approach doesn't compromise human usability—in fact, the clarity required for AI generation makes the language more intuitive for developers as well.
Contractual Integrity
The data model serves as a formal contract between the storage layer, the application code, and downstream analytics. It ensures that the meaning of data is preserved across stack boundaries.
Access-Pattern Modeling
Unlike standard DDL, Capacitor models are driven by how the application interacts with data. It treats indexes, partitions, and relationships as logical requirements rather than physical implementation details.
Infrastructure Independence
By abstracting the data model, Capacitor eliminates vendor lock-in. A model defined in Capacitor can migrate from a relational structure to a distributed NoSQL cluster without rewriting the application's data access layer.
Progressive Enhancement
Features are defined semantically in Capacitor. Generators implement them using native database capabilities when available, or generate shims when needed. You get clear feedback at generation time about implementation trade-offs.
Namespace and Module System
Overview
Every Capacitor model file must begin with a namespace declaration. Namespaces provide module scoping, prevent naming collisions, and enable dependency management across project boundaries. They are essential for building reusable type libraries and generating properly packaged code for target languages.
Namespace Declaration
Every .capacitor file MUST begin with a namespace declaration using reverse domain notation:
namespace com.acme.data.users
Namespace rules:
- Minimum two segments required (
com.acme, not justacme) - Use reverse domain notation (like Java packages, Smithy namespaces)
- Multiple files MAY share the same namespace (types are merged at validation time)
- The namespace declaration is authoritative (directory structure is convention, not requirement)
Namespace mapping to target languages:
| Language | Namespace Mapping | Example |
|---|---|---|
| Java | Package | com.acme.data.users → package com.acme.data.users |
| TypeScript | npm scope + module | com.acme.data.users → @acme/data-users |
| Go | Module path | com.acme.data.users → github.com/acme/data/users |
| C# | Namespace | com.acme.data.users → Acme.Data.Users |
| Python | Module | com.acme.data.users → acme.data.users |
| Rust | Crate | com.acme.data.users → acme_data_users |
| Swift | Module | com.acme.data.users → AcmeDataUsers |
| Scala | Package | com.acme.data.users → com.acme.data.users |
Generator Note: The build system (capacitor-build.json) allows configuring custom namespace-to-package mappings per language. See Build Configuration for details.
Imports
Types from other namespaces are brought into scope with use:
namespace com.acme.data.orders
use com.acme.data.users#User
use com.acme.data.users#UserStatus
use com.acme.data.common#Address
use com.acme.data.common#AuditFields
Import rules:
#separates namespace from type name (matches Smithy convention)- Each type must be imported explicitly
- Wildcard imports are NOT supported (explicit is better than implicit for AI generation and human readability)
- Circular imports between namespaces are a validation error
Grouped Imports
Multiple types from the same namespace can be imported together:
namespace com.acme.data.orders
use com.acme.data.common#{ Currency, Money, Address, ContactInfo }
use com.acme.data.users#{ User, UserStatus, UserPreferences }
Aliased Imports
When type names collide, use aliased imports:
namespace com.acme.data.logistics
use com.acme.data.shipping#Address as ShippingAddress
use com.acme.data.billing#Address as BillingAddress
entity Shipment {
@identity
shipmentId: UUID,
origin: ShippingAddress,
destination: ShippingAddress,
billingAddress: BillingAddress
}
Prelude Namespace
Capacitor provides a built-in prelude namespace (capacitor.prelude) that is automatically imported into every file. This includes all scalar types, semantic types, and collection types:
Prelude types (no explicit import needed):
- Scalars:
String,Integer,Long,Float,Double,Decimal,Boolean,Blob,Document - Semantic:
Email,UUID,Timestamp,Date,Time,Currency,Location,URL,Phone - Collections:
List<T>,Set<T>,Map<K, V>
Custom types from your models or external dependencies ALWAYS require explicit imports.
File and Directory Convention
While not enforced, the recommended convention is to mirror the namespace structure in your directory layout:
models/
├── com/
│ └── acme/
│ └── data/
│ ├── common/
│ │ ├── types.capacitor // namespace com.acme.data.common
│ │ └── audit.capacitor // namespace com.acme.data.common
│ ├── users/
│ │ ├── user.capacitor // namespace com.acme.data.users
│ │ └── preferences.capacitor // namespace com.acme.data.users
│ └── orders/
│ ├── order.capacitor // namespace com.acme.data.orders
│ └── payment.capacitor // namespace com.acme.data.orders
Note: Multiple files can share a namespace. Types from all files with the same namespace are merged at validation time into a single logical namespace.
External Dependencies
Projects can depend on published Capacitor model packages. Dependencies are declared in capacitor-build.json:
{
"dependencies": {
"com.acme.data.common": {
"version": "1.2.0",
"repository": "https://repo.acme.com/capacitor"
},
"org.capacitor.stdlib.geo": {
"version": "1.0.0"
}
}
}
The dependency resolver:
- Fetches published
.capacitormodel packages from configured repositories - Makes their types available for
useimports - Validates version compatibility
- Supports semantic versioning with range constraints (
^1.2.0,~1.2.0,>=1.0.0 <2.0.0)
Example using an external dependency:
namespace com.acme.data.stores
// Import from external dependency
use org.capacitor.stdlib.geo#GeoPoint
use org.capacitor.stdlib.geo#GeoPolygon
entity Store {
@identity
storeId: UUID,
name: String,
// Use imported type from external package
location: GeoPoint,
// Delivery zone boundary
deliveryArea: GeoPolygon?
}
Best Practices
- Use reverse domain notation: Ensures global uniqueness across organizations
- Organize by domain/bounded context: Group related entities in the same namespace
- Keep namespaces shallow: 3-4 segments is ideal (
com.acme.data.users), avoid deep nesting - Explicit imports: Always import types explicitly, never rely on wildcards
- Namespace per service: Each microservice's data model should have its own root namespace
- Version external dependencies: Use semantic versioning constraints in dependencies
Generator Note: Namespaces are critical for multi-project codebases and library publishing. Generators use namespace information to produce properly packaged artifacts (JARs, npm packages, Python wheels, etc.) with correct module structures and dependencies.
Core Building Blocks
Entities
Entities are the primary units of Capacitor. They represent stateful objects that persist over time and maintain a unique identity.
entity User {
@identity
userId: UUID,
username: String,
email: Email,
createdAt: Timestamp
}
Shapes
Shapes are reusable structures that do not have their own independent identity. They are used to group related fields within an Entity to ensure consistency across the model.
shape Address {
street: String,
city: String,
state: String,
zipCode: String,
country: String
}
entity Warehouse {
@identity
id: UUID,
name: String,
location: Address
}
Enums
Enums define a fixed set of valid values for a field, providing type safety and semantic clarity.
enum OrderStatus {
PENDING,
CONFIRMED,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED,
REFUNDED
}
entity Order {
@identity
orderId: UUID,
status: OrderStatus,
total: Decimal
}
Generator Note: Implementation varies by database. Native enums in SQL, validation rules in NoSQL. See Generator Guide.
Traits
Traits are metadata modifiers prefixed with @. They allow architects to attach "capabilities" to data fields—such as encryption, indexing, or searchability—without needing to know the underlying database's syntax for those features.
Type System
Capacitor provides a Standard Library (LSL) of semantic types that map to the most efficient underlying platform types while providing built-in validation.
Prelude Types: All scalar types, semantic types, and collection types documented in this section are part of the capacitor.prelude namespace and are automatically imported into every .capacitor file. You never need to explicitly import these built-in types. Custom types (entities, shapes, enums) from your models or external dependencies always require explicit use imports.
Scalar Types
| Capacitor Type | Description |
|---|---|
String | UTF-8 encoded text |
Integer | 32-bit signed integer |
Long | 64-bit signed integer |
Float | 32-bit floating point |
Double | 64-bit floating point |
Decimal | Arbitrary precision decimal (for currency, etc.) |
Boolean | Logical true/false |
Blob | Raw binary data |
Document | Semi-structured dynamic data (JSON) |
Semantic Types
These types include built-in validation rules and formatting requirements:
- Email: Validated string matching RFC 5322
- UUID: 128-bit unique identifier (v4 by default)
- Timestamp: High-precision time (UTC, ISO 8601)
- Date: Calendar date without time component
- Time: Time of day without date
- Currency: Composite of Decimal amount and ISO 4217 currency code
- Location: Geographic coordinates (Latitude/Longitude)
- URL: Validated URL string
- Phone: E.164 formatted phone number
Collection Types
- List<T>: Ordered collection with duplicates allowed
- Set<T>: Unordered collection of unique elements
- Map<K, V>: Key-value pairs with unique keys
Nullable Types
By default, all fields are required (not null). Use ? suffix to make a field optional.
entity Profile {
@identity
id: UUID,
name: String, // Required
bio: String?, // Optional
avatarUrl: URL? // Optional
}
Identity and Access Modeling
Capacitor treats identity as an abstract "Key Shape." The generator translates these logical keys into the optimal physical structure.
Primary Keys
Single-field identity:
entity User {
@identity
userId: UUID,
username: String
}
Compound identity:
entity TimeSeriesData {
@identity @partition
deviceId: UUID,
@identity @sort
timestamp: Timestamp,
value: Double
}
Key Traits
- @identity: Marks a field as part of the unique identifier
- @partition: Defines the horizontal scaling boundary (sharding key)
- @sort: Defines the physical ordering within a partition
Access Patterns (Indexes)
Instead of manual index management, developers describe the Access Pattern required by the business logic.
Global access (query by non-identity field):
entity Product {
@identity
sku: String,
@access(name: "ByCategory", type: "global")
category: String,
name: String,
price: Decimal
}
Local access (alternate ordering within partition):
entity Task {
@identity @partition
projectId: UUID,
@identity @sort
taskId: UUID,
@access(name: "ByPriority", type: "local", sort: "priority")
priority: Integer,
title: String,
status: String
}
Composite access (multi-field index):
entity Customer {
@identity
id: UUID,
@access(name: "ByLocationAndName", type: "global")
@composite(fields: ["state", "city", "lastName"])
searchKey: String,
firstName: String,
lastName: String,
city: String,
state: String
}
Sparse/Partial access (conditional index):
entity Order {
@identity
orderId: UUID,
// Only indexes orders where isFlagged is true
@access(name: "FlaggedOrders", type: "global")
@sparse
isFlagged: Boolean?,
amount: Decimal
}
Full-text search:
entity Article {
@identity
id: UUID,
@access(name: "SearchContent", type: "fulltext")
@fulltext(language: "english")
content: String,
title: String,
author: String
}
Generator Note: Full-text implementation varies. PostgreSQL uses native full-text search, MongoDB uses text indexes, DynamoDB requires external search service. See Generator Guide.
Best Practices
- Limit Global Access: Every global access pattern adds write latency
- Prefer Local Access: Query within partition boundaries for maximum performance
- Semantic Naming: Name access patterns after business intent (e.g.,
ByEmail) not implementation (e.g.,Index1)
Constraints and Validation
Capacitor provides semantic constraints that are enforced at the appropriate layer based on the target database.
Unique Constraints
entity User {
@identity
userId: UUID,
@unique
username: String,
@unique
email: Email
}
Default Values
entity Product {
@identity
sku: String,
name: String,
@default(value: "0")
inventory: Integer,
@default(value: "true")
isActive: Boolean,
@default(value: "now()")
createdAt: Timestamp
}
Check Constraints
entity Product {
@identity
sku: String,
@check(condition: "price > 0")
price: Decimal,
@check(condition: "inventory >= 0")
inventory: Integer
}
Length Constraints
entity Post {
@identity
id: UUID,
@length(min: 3, max: 100)
title: String,
@length(max: 5000)
content: String
}
Range Constraints
entity Rating {
@identity
id: UUID,
@range(min: 1, max: 5)
stars: Integer,
@range(min: 0.0, max: 100.0)
confidenceScore: Double
}
Pattern Constraints
entity Product {
@identity
id: UUID,
@pattern(regex: "^[A-Z]{2}\\d{6}$")
sku: String // Example: AB123456
}
Generator Note: Check constraints, length, and range validations are implemented natively in SQL databases. For NoSQL databases, they are enforced in the generated SDK. See Generator Guide.
Relationships
Capacitor uses the @link trait to define how entities relate. The implementation is handled by generators based on the target database.
One-to-One Relationships
entity Passport {
@identity
passportNumber: String,
expiryDate: Date,
issuingCountry: String
}
entity Citizen {
@identity
ssn: String,
name: String,
@link(to: Passport)
passport: Passport
}
One-to-Many Relationships
entity Author {
@identity
authorId: UUID,
name: String
}
entity Book {
@identity
isbn: String,
@link(to: Author, on: "authorId")
author: Author,
title: String,
publishedDate: Date
}
Alternative syntax (parent reference):
entity Order {
@identity
orderId: UUID,
@link(to: User, on: "userId")
customer: User,
total: Decimal
}
Many-to-Many Relationships
entity Student {
@identity
studentId: UUID,
name: String,
@link(to: Course, cardinality: "many-to-many")
enrollments: List<Course>
}
entity Course {
@identity
courseId: String,
title: String,
@link(to: Student, cardinality: "many-to-many")
roster: List<Student>
}
Cascading Operations
entity Folder {
@identity
id: UUID,
name: String,
@link(to: File, onDelete: "cascade")
files: List<File>
}
entity File {
@identity
id: UUID,
name: String,
size: Long
}
Cascade options:
cascade: Delete related recordsset_null: Set foreign key to nullrestrict: Prevent deletion if references existno_action: Database default behavior
Generator Note: Referential integrity implementation varies. SQL databases use native foreign keys. NoSQL databases implement checks in the SDK. See Generator Guide.
Mixins and Behavioral Composition
Mixins provide a way to compose entity behavior through reusable field and trait combinations. Unlike shapes (which are purely structural), mixins carry semantic meaning that generators understand and implement with framework-specific patterns.
Understanding Mixins vs. Shapes
Shapes are structural reuse — they define a group of fields that can be embedded in multiple entities:
shape Address {
street: String,
city: String,
state: String,
zipCode: String
}
entity User {
@identity userId: UUID,
homeAddress: Address // Embedded shape
}
Mixins are behavioral reuse — they define fields plus associated behaviors that generators implement:
mixin Auditable {
@readonly
@default(value: "now()")
createdAt: Timestamp,
@readonly
createdBy: UUID?,
@default(value: "now()")
updatedAt: Timestamp,
updatedBy: UUID?
}
entity User with Auditable { // Behavioral composition
@identity userId: UUID,
username: String
// Inherits audit fields plus update behavior
}
Key difference: Generators recognize mixins and generate framework-specific interceptors, triggers, or event listeners to automatically maintain the mixin's behavior.
Defining Mixins
Mixins are defined with the mixin keyword:
namespace com.acme.data.common
/// Adds creation and update tracking to any entity.
/// Generators produce JPA @PrePersist/@PreUpdate listeners,
/// SQLAlchemy event hooks, or equivalent mechanisms.
mixin Auditable {
@readonly
@default(value: "now()")
createdAt: Timestamp,
@readonly
createdBy: UUID?,
@default(value: "now()")
updatedAt: Timestamp,
updatedBy: UUID?
}
Common Built-in Mixins
Auditable Mixin
Tracks creation and modification metadata:
mixin Auditable {
@readonly
@default(value: "now()")
createdAt: Timestamp,
@readonly
createdBy: UUID?,
@default(value: "now()")
updatedAt: Timestamp,
updatedBy: UUID?
}
Generated behavior:
- JPA:
@PrePersistand@PreUpdateentity listeners - SQLAlchemy:
before_insertandbefore_updateevent listeners - TypeORM:
@BeforeInsertand@BeforeUpdatedecorators - Entity Framework:
SaveChangesinterceptor - Prisma: Middleware hooks
SoftDeletable Mixin
Enables logical deletion instead of physical deletion:
mixin SoftDeletable {
deletedAt: Timestamp?,
deletedBy: UUID?,
@computed(expression: "deletedAt == null")
isActive: Boolean
}
Generated behavior:
- Generators add WHERE clauses to all queries:
WHERE deletedAt IS NULL - Repository
delete()method setsdeletedAtinstead of removing the record - Generators provide
restore()methods on repositories - JPA: Hibernate
@Whereannotation or@FilterDef - SQLAlchemy: Query filter
- Prisma: Middleware to filter soft-deleted records
Versioned Mixin
Enables optimistic concurrency control:
mixin Versioned {
@version
version: Long
}
Generated behavior:
- JPA:
@Versionannotation triggers automatic version checking - SQLAlchemy: Version tracking with
version_id_col - Entity Framework:
IsConcurrencyTokenon version property - DynamoDB: Conditional updates based on version field
- Updates fail if version doesn't match (optimistic locking)
TenantScoped Mixin
Adds multi-tenant isolation:
mixin TenantScoped {
@tenant
@readonly
tenantId: UUID
}
Generated behavior:
- Generators automatically add
tenantIdfilter to all queries - Repository methods require tenant context
- JPA: Hibernate filters based on tenant ID
- SQLAlchemy: Scoped sessions with tenant filter
- Prisma: Middleware to inject tenant filter
- Row-Level Security (PostgreSQL): RLS policies based on tenant ID
Applying Mixins to Entities
Use the with keyword to compose mixins into an entity:
namespace com.acme.data.users
use com.acme.data.common#Auditable
use com.acme.data.common#SoftDeletable
use com.acme.data.common#Versioned
entity User with Auditable, SoftDeletable, Versioned {
@identity
userId: UUID,
username: String,
email: Email,
status: UserStatus
// Inherits from mixins:
// - createdAt, createdBy, updatedAt, updatedBy (Auditable)
// - deletedAt, deletedBy, isActive (SoftDeletable)
// - version (Versioned)
}
Generated Java entity:
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE user_id = ? AND version = ?")
@Where(clause = "deleted_at IS NULL")
public class User {
@Id
private UUID userId;
private String username;
private String email;
@Enumerated(EnumType.STRING)
private UserStatus status;
// From Auditable mixin
@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;
@CreatedBy
@Column(updatable = false)
private UUID createdBy;
@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
@LastModifiedBy
private UUID updatedBy;
// From SoftDeletable mixin
private Instant deletedAt;
private UUID deletedBy;
// From Versioned mixin
@Version
private Long version;
// Computed field from SoftDeletable
public Boolean getIsActive() {
return deletedAt == null;
}
}
Multiple Mixin Composition
Entities can compose multiple mixins:
entity Order with Auditable, SoftDeletable, Versioned, TenantScoped {
@identity
orderId: UUID,
customerId: UUID,
total: Decimal,
status: OrderStatus
// Inherits fields and behaviors from all four mixins
}
Custom Mixins
Define domain-specific mixins for your use cases:
namespace com.acme.data.common
/// Adds approval workflow fields to any entity.
mixin Approvable {
approvalStatus: ApprovalStatus,
approvedAt: Timestamp?,
approvedBy: UUID?,
rejectedAt: Timestamp?,
rejectedBy: UUID?,
rejectionReason: String?
}
enum ApprovalStatus {
PENDING,
APPROVED,
REJECTED
}
Usage:
entity ExpenseReport with Auditable, Approvable {
@identity
reportId: UUID,
employeeId: UUID,
totalAmount: Decimal,
description: String
// Inherits audit tracking and approval workflow
}
Mixin Conflicts
If two mixins define the same field name, you must resolve the conflict explicitly:
mixin MixinA {
status: String
}
mixin MixinB {
status: Integer
}
// Validation error: field 'status' conflict
entity MyEntity with MixinA, MixinB {
@identity id: UUID
}
Resolution:
entity MyEntity with MixinA, MixinB {
@identity id: UUID,
// Override to resolve conflict
@override
status: String // Explicitly choose MixinA's definition
}
Mixin Inheritance
Mixins can compose other mixins:
mixin Timestamped {
@readonly
@default(value: "now()")
createdAt: Timestamp,
@default(value: "now()")
updatedAt: Timestamp
}
mixin Auditable extends Timestamped {
// Includes createdAt, updatedAt from Timestamped
@readonly
createdBy: UUID?,
updatedBy: UUID?
}
Best Practices
- Single Responsibility: Each mixin should represent one behavioral concern
- Naming Convention: Use adjective names ending in "-able" (Auditable, Versionable, Approvable)
- Keep Mixins Focused: Limit mixins to 3-5 fields plus their associated behavior
- Document Behavior: Clearly document what automatic behavior the mixin provides
- Standard Library: Build a library of standard mixins for your organization
- Avoid Deep Inheritance: Don't nest mixin inheritance more than 2 levels deep
- Conflict Resolution: Explicitly resolve any field name conflicts
Generator Note: Mixins are a powerful abstraction that generators translate into framework-specific patterns. The same mixin definition generates JPA entity listeners, SQLAlchemy events, TypeORM decorators, or Entity Framework interceptors depending on the target. This allows you to define behavior once at the semantic level and have it correctly implemented across all platforms. See Generator Guide for implementation details per framework.
Views and Aggregations
Views
Views represent virtual tables derived from queries. They are computed on-demand when queried.
view ActiveUsers {
source: User,
filter: "status == 'active' AND lastLogin > now() - 30d",
fields: [userId, username, email, lastLogin]
}
Complex view with joins:
view OrderSummary {
source: [Order, Customer],
join: "Order.customerId == Customer.id",
fields: [
Order.orderId,
Order.total,
Order.createdAt,
Customer.name as customerName,
Customer.email as customerEmail
],
filter: "Order.status != 'CANCELLED'"
}
Materialized Views
Materialized views are pre-computed and stored query results that improve performance for expensive queries.
@materialized(
refresh: "incremental", // or "full"
schedule: "daily" // or "hourly", "on_change", etc.
)
view CustomerOrderStats {
source: [Customer, Order],
join: "Order.customerId == Customer.id",
aggregate: {
groupBy: ["Customer.id", "Customer.name"],
metrics: {
totalOrders: count(Order.orderId),
totalRevenue: sum(Order.amount),
avgOrderValue: avg(Order.amount),
lastOrderDate: max(Order.createdAt)
}
}
}
With filter:
@materialized(refresh: "incremental", schedule: "on_change")
view RecentHighValueOrders {
source: Order,
filter: "amount > 1000 AND createdAt > now() - 90d",
aggregate: {
groupBy: ["customerId"],
metrics: {
orderCount: count(orderId),
totalSpent: sum(amount)
}
}
}
Refresh strategies:
full: Rebuild entire view from scratchincremental: Update only changed dataon_change: Refresh when source data changes (real-time)scheduled: Refresh at specified intervals
Generator Note: Materialized views are a complex feature with varying support across databases. PostgreSQL and some SQL databases have native support. NoSQL databases require generator shims. See Generator Guide for implementation details and performance considerations.
Documentation and Annotations
Capacitor supports comprehensive documentation annotations that are preserved in generated code and can be used to produce API documentation, user guides, and interactive explorers.
Inline Documentation with Triple-Slash Comments
Use triple-slash line comments (///) to document entities, fields, and other constructs. This is the preferred method for field-level documentation:
/// Represents a registered user in the system.
///
/// Users are created via the registration API and must have
/// a verified email before they can access premium features.
entity User {
@identity
/// System-generated unique identifier. Immutable after creation.
userId: UUID,
/// Must be 3-50 characters. Alphanumeric and underscores only.
/// Displayed publicly on the user's profile page.
@unique
username: String,
/// Primary contact email. Must be verified within 24 hours of registration.
@pii
email: Email,
/// Account lifecycle status. Transitions: PENDING → ACTIVE → SUSPENDED → INACTIVE.
status: UserStatus,
/// Server-generated timestamp. Cannot be modified after creation.
@default(value: "now()")
createdAt: Timestamp
}
Generator Note: Triple-slash comments are converted to target language documentation formats:
- Java: Javadoc comments (
/** ... */) - TypeScript: JSDoc comments (
/** ... */) - Python: Docstrings (
"""...""") - C#: XML documentation comments (
/// <summary>...</summary>) - Go: Godoc comments (
//...) - Swift: Doc comments (
/// ...)
Structured Documentation with @documentation Trait
For more structured metadata, use the @documentation trait:
@documentation(
summary: "Tracks all financial transactions in the system",
since: "1.0",
authors: ["data-platform-team"],
seeAlso: ["com.acme.data.accounting#Ledger"],
examples: [
{
title: "Creating a debit transaction",
code: """
Transaction tx = repo.create(
Transaction.builder()
.type(TransactionType.DEBIT)
.amount(Money.of(100, "USD"))
.build()
);
"""
}
]
)
entity Transaction {
@identity
transactionId: UUID,
type: TransactionType,
amount: Decimal,
currency: String,
createdAt: Timestamp
}
Supported metadata fields:
summary: Brief description (1-2 sentences)description: Detailed explanation (markdown supported)since: Version when introduced (e.g., "1.0", "2.1.0")deprecated: Deprecation message and migration pathauthors: List of responsible teams/individualsseeAlso: Related types or documentation (fully qualified names)examples: Array of usage examples with title and codetags: Custom categorization tags
Usage Examples with @example Trait
Attach specific usage examples to fields, queries, or other constructs:
@query(
name: "findHighValueCustomers",
entity: Order,
aggregate: {
groupBy: ["customerId"],
having: "SUM(Order.total) > :threshold",
metrics: {
totalSpent: sum(Order.total),
orderCount: count(Order.orderId)
}
},
params: {
threshold: Decimal
}
)
@example(
description: "Find customers who have spent more than $10,000",
params: { threshold: "10000.00" },
notes: "Consider running during off-peak hours for large datasets"
)
Documentation Generation
Documentation can be generated in multiple formats via capacitor-build.json:
{
"documentation": {
"html": {
"format": "html",
"output": "./docs/api",
"options": {
"title": "User Service Data Layer API",
"includeERDiagram": true,
"includeDatabaseMapping": true,
"includeAccessPatternDocs": true,
"includeConstraintDocs": true,
"includeExamples": true,
"includeGeneratorNotes": true,
"searchEnabled": true,
"theme": "default"
}
},
"markdown": {
"format": "markdown",
"output": "./docs/md",
"options": {
"singleFile": false,
"includeTableOfContents": true
}
}
}
}
Generated documentation includes:
- Model Overview — All entities, shapes, enums with summaries
- Entity Detail Pages — Fields, constraints, relationships, access patterns
- ER Diagram — Auto-generated entity-relationship diagram (Mermaid/PlantUML/D2)
- Access Pattern Reference — All indexes with types, fields, performance notes
- Relationship Graph — Visual map of all
@linkrelationships - Constraint Catalog — All validation rules organized by entity
- Query Catalog — Named queries and query builders with code examples
- Migration History — Chronological list of schema changes
- Generator Compatibility Matrix — Feature support (native vs. shimmed) per target
- Type Reference — All scalar, semantic, and custom types with validation
- Glossary — Auto-generated from entity and field doc comments
Documentation in Generated Code
All generated code includes comprehensive inline documentation derived from your Capacitor models:
Java (Javadoc):
/**
* Represents a registered user in the system.
*
* <p>Users are created via the registration API and must have
* a verified email before they can access premium features.</p>
*
* <h3>Access Patterns:</h3>
* <ul>
* <li>{@code ByUsername} - Global index on username (unique)</li>
* <li>{@code ByEmail} - Global index on email</li>
* </ul>
*
* <h3>Constraints:</h3>
* <ul>
* <li>username: unique, 3-50 characters</li>
* <li>email: unique, RFC 5322 validated</li>
* </ul>
*
* @since 1.0
* @see UserRepository
* @generated by Capacitor v1.0 — do not edit manually
*/
@Entity
@Table(name = "users")
public class User { ... }
TypeScript (JSDoc):
/**
* Represents a registered user in the system.
*
* Users are created via the registration API and must have
* a verified email before they can access premium features.
*
* @since 1.0
* @see {@link UserRepository}
* @generated by Capacitor v1.0 — do not edit manually
*/
export interface User {
/**
* System-generated unique identifier. Immutable after creation.
*/
userId: string;
/**
* Must be 3-50 characters. Alphanumeric and underscores only.
* Displayed publicly on the user's profile page.
*/
username: string;
// ...
}
Best Practices
- Document intent, not implementation: Explain why a field exists, not just what it is
- Be specific about constraints: Document validation rules, allowed values, and transitions
- Include examples: Show common usage patterns and edge cases
- Reference related types: Use
seeAlsoto link to related entities and documentation - Mark deprecations early: Use
deprecatedto give consumers time to migrate - Keep it current: Update documentation when schema changes
- Document computed fields: Explain the calculation and when it's updated
Generator Note: Documentation generation is available for all target databases and languages. The quality and depth of generated documentation directly correlates with the quality of your inline Capacitor documentation.
Query Definitions
Query definitions allow you to declare reusable, named queries as part of your data model. These queries are translated to framework-specific implementations, generating JPA annotations for Java, SQLAlchemy queries for Python, TypeORM/Prisma for Node.js, Entity Framework for .NET, and Core Data/GRDB for Swift.
Named Queries
Named queries define reusable query logic that can be referenced by name in application code.
entity User {
@identity
userId: UUID,
username: String,
email: Email,
status: String,
lastLoginAt: Timestamp?,
createdAt: Timestamp
}
@query(
name: "findActiveUsers",
entity: User,
filter: "status == 'ACTIVE'",
orderBy: ["lastLoginAt DESC"]
)
@query(
name: "findUsersByEmailDomain",
entity: User,
filter: "email LIKE :domain",
params: {
domain: String
}
)
@query(
name: "findRecentUsers",
entity: User,
filter: "createdAt > :since",
params: {
since: Timestamp
},
orderBy: ["createdAt DESC"],
limit: 100
)
Generated JPA (Java/Spring):
@Entity
@NamedQueries({
@NamedQuery(
name = "User.findActiveUsers",
query = "SELECT u FROM User u WHERE u.status = 'ACTIVE' ORDER BY u.lastLoginAt DESC"
),
@NamedQuery(
name = "User.findUsersByEmailDomain",
query = "SELECT u FROM User u WHERE u.email LIKE :domain"
),
@NamedQuery(
name = "User.findRecentUsers",
query = "SELECT u FROM User u WHERE u.createdAt > :since ORDER BY u.createdAt DESC"
)
})
public class User {
@Id
private UUID userId;
private String username;
private String email;
private String status;
private Timestamp lastLoginAt;
private Timestamp createdAt;
}
// Repository interface
public interface UserRepository extends JpaRepository<User, UUID> {
@Query(name = "User.findActiveUsers")
List<User> findActiveUsers();
@Query(name = "User.findUsersByEmailDomain")
List<User> findUsersByEmailDomain(@Param("domain") String domain);
@Query(name = "User.findRecentUsers")
List<User> findRecentUsers(@Param("since") Timestamp since, Pageable pageable);
}
Generated SQLAlchemy (Python):
from sqlalchemy import select, and_
from datetime import datetime
class UserQueries:
@staticmethod
def find_active_users(session):
return session.execute(
select(User)
.where(User.status == 'ACTIVE')
.order_by(User.last_login_at.desc())
).scalars().all()
@staticmethod
def find_users_by_email_domain(session, domain: str):
return session.execute(
select(User)
.where(User.email.like(domain))
).scalars().all()
@staticmethod
def find_recent_users(session, since: datetime):
return session.execute(
select(User)
.where(User.created_at > since)
.order_by(User.created_at.desc())
.limit(100)
).scalars().all()
Generated TypeORM (Node.js/TypeScript):
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
userId: string;
@Column()
username: string;
@Column()
email: string;
@Column()
status: string;
@Column({ nullable: true })
lastLoginAt: Date;
@Column()
createdAt: Date;
}
export class UserQueries {
static findActiveUsers(repository: Repository<User>): Promise<User[]> {
return repository
.createQueryBuilder('user')
.where("user.status = :status", { status: 'ACTIVE' })
.orderBy('user.lastLoginAt', 'DESC')
.getMany();
}
static findUsersByEmailDomain(
repository: Repository<User>,
domain: string
): Promise<User[]> {
return repository
.createQueryBuilder('user')
.where("user.email LIKE :domain", { domain })
.getMany();
}
static findRecentUsers(
repository: Repository<User>,
since: Date
): Promise<User[]> {
return repository
.createQueryBuilder('user')
.where("user.createdAt > :since", { since })
.orderBy('user.createdAt', 'DESC')
.limit(100)
.getMany();
}
}
Generated Entity Framework (.NET/C#):
public class UserQueries
{
public static IQueryable<User> FindActiveUsers(DbContext context)
{
return context.Set<User>()
.Where(u => u.Status == "ACTIVE")
.OrderByDescending(u => u.LastLoginAt);
}
public static IQueryable<User> FindUsersByEmailDomain(
DbContext context,
string domain
)
{
return context.Set<User>()
.Where(u => EF.Functions.Like(u.Email, domain));
}
public static IQueryable<User> FindRecentUsers(
DbContext context,
DateTime since
)
{
return context.Set<User>()
.Where(u => u.CreatedAt > since)
.OrderByDescending(u => u.CreatedAt)
.Take(100);
}
}
Generated Core Data (Swift):
extension User {
static func findActiveUsers(context: NSManagedObjectContext) -> [User] {
let request: NSFetchRequest<User> = User.fetchRequest()
request.predicate = NSPredicate(format: "status == %@", "ACTIVE")
request.sortDescriptors = [
NSSortDescriptor(key: "lastLoginAt", ascending: false)
]
return (try? context.fetch(request)) ?? []
}
static func findUsersByEmailDomain(
context: NSManagedObjectContext,
domain: String
) -> [User] {
let request: NSFetchRequest<User> = User.fetchRequest()
request.predicate = NSPredicate(format: "email LIKE %@", domain)
return (try? context.fetch(request)) ?? []
}
static func findRecentUsers(
context: NSManagedObjectContext,
since: Date
) -> [User] {
let request: NSFetchRequest<User> = User.fetchRequest()
request.predicate = NSPredicate(format: "createdAt > %@", since as NSDate)
request.sortDescriptors = [
NSSortDescriptor(key: "createdAt", ascending: false)
]
request.fetchLimit = 100
return (try? context.fetch(request)) ?? []
}
}
Complex Queries with Joins
Queries can span multiple entities using joins.
entity Order {
@identity
orderId: UUID,
@link(to: User, on: "userId")
customer: User,
total: Decimal,
status: String,
createdAt: Timestamp
}
@query(
name: "findUserOrderHistory",
entity: Order,
join: [
{entity: User, on: "Order.customer == User.userId"}
],
filter: "User.userId == :userId",
params: {
userId: UUID
},
orderBy: ["Order.createdAt DESC"],
select: [
"Order.orderId",
"Order.total",
"Order.status",
"Order.createdAt",
"User.username",
"User.email"
]
)
@query(
name: "findHighValueCustomers",
entity: Order,
join: [
{entity: User, on: "Order.customer == User.userId"}
],
aggregate: {
groupBy: ["User.userId", "User.username", "User.email"],
having: "SUM(Order.total) > :threshold",
metrics: {
totalSpent: sum(Order.total),
orderCount: count(Order.orderId),
avgOrderValue: avg(Order.total)
}
},
params: {
threshold: Decimal
},
orderBy: ["totalSpent DESC"]
)
Generated JPA (Java/Spring):
public interface OrderRepository extends JpaRepository<Order, UUID> {
@Query("""
SELECT new com.example.OrderHistoryDto(
o.orderId, o.total, o.status, o.createdAt,
u.username, u.email
)
FROM Order o
JOIN o.customer u
WHERE u.userId = :userId
ORDER BY o.createdAt DESC
""")
List<OrderHistoryDto> findUserOrderHistory(@Param("userId") UUID userId);
@Query("""
SELECT new com.example.CustomerValueDto(
u.userId, u.username, u.email,
SUM(o.total) as totalSpent,
COUNT(o.orderId) as orderCount,
AVG(o.total) as avgOrderValue
)
FROM Order o
JOIN o.customer u
GROUP BY u.userId, u.username, u.email
HAVING SUM(o.total) > :threshold
ORDER BY totalSpent DESC
""")
List<CustomerValueDto> findHighValueCustomers(@Param("threshold") BigDecimal threshold);
}
Generated SQLAlchemy (Python):
class OrderQueries:
@staticmethod
def find_user_order_history(session, user_id: UUID):
return session.execute(
select(
Order.order_id,
Order.total,
Order.status,
Order.created_at,
User.username,
User.email
)
.join(User, Order.customer_id == User.user_id)
.where(User.user_id == user_id)
.order_by(Order.created_at.desc())
).all()
@staticmethod
def find_high_value_customers(session, threshold: Decimal):
return session.execute(
select(
User.user_id,
User.username,
User.email,
func.sum(Order.total).label('total_spent'),
func.count(Order.order_id).label('order_count'),
func.avg(Order.total).label('avg_order_value')
)
.join(User, Order.customer_id == User.user_id)
.group_by(User.user_id, User.username, User.email)
.having(func.sum(Order.total) > threshold)
.order_by(func.sum(Order.total).desc())
).all()
Pagination Queries
Define queries with built-in pagination support.
@query(
name: "findAllUsersPaginated",
entity: User,
orderBy: ["username ASC"],
pagination: {
type: "offset", // or "cursor"
defaultPageSize: 20,
maxPageSize: 100
}
)
@query(
name: "findUsersByCursor",
entity: User,
orderBy: ["createdAt DESC", "userId ASC"],
pagination: {
type: "cursor",
cursorFields: ["createdAt", "userId"]
}
)
Generated Spring Data (Java):
public interface UserRepository extends JpaRepository<User, UUID> {
@Query("SELECT u FROM User u ORDER BY u.username ASC")
Page<User> findAllUsersPaginated(Pageable pageable);
// Cursor-based pagination with Slice
@Query("SELECT u FROM User u WHERE u.createdAt < :cursor ORDER BY u.createdAt DESC, u.userId ASC")
Slice<User> findUsersByCursor(
@Param("cursor") Timestamp cursor,
Pageable pageable
);
}
Generated Prisma (Node.js):
export class UserQueries {
static async findAllUsersPaginated(
prisma: PrismaClient,
page: number = 1,
pageSize: number = 20
) {
const skip = (page - 1) * pageSize;
const take = Math.min(pageSize, 100);
const [users, total] = await Promise.all([
prisma.user.findMany({
skip,
take,
orderBy: { username: 'asc' }
}),
prisma.user.count()
]);
return {
data: users,
page,
pageSize: take,
total,
totalPages: Math.ceil(total / take)
};
}
static async findUsersByCursor(
prisma: PrismaClient,
cursor?: { createdAt: Date; userId: string },
pageSize: number = 20
) {
return prisma.user.findMany({
take: Math.min(pageSize, 100),
...(cursor && {
cursor: { userId: cursor.userId },
skip: 1 // Skip the cursor
}),
where: cursor ? {
createdAt: { lt: cursor.createdAt }
} : undefined,
orderBy: [
{ createdAt: 'desc' },
{ userId: 'asc' }
]
});
}
}
Dynamic Query Builders
For queries that need runtime composition, Capacitor generates type-safe query builders.
@queryBuilder(
name: "UserSearchBuilder",
entity: User,
filters: {
username: { type: String, operator: ["==", "LIKE"] },
email: { type: Email, operator: ["==", "LIKE"] },
status: { type: String, operator: ["==", "IN"] },
createdAt: { type: Timestamp, operator: [">=", "<=", "BETWEEN"] },
lastLoginAt: { type: Timestamp, operator: [">=", "<=", "IS NULL"] }
},
sortable: ["username", "email", "createdAt", "lastLoginAt"],
pagination: true
)
Generated TypeScript (TypeORM):
export class UserSearchBuilder {
private queryBuilder: SelectQueryBuilder<User>;
constructor(repository: Repository<User>) {
this.queryBuilder = repository.createQueryBuilder('user');
}
whereUsername(value: string, operator: '==' | 'LIKE' = '=='): this {
if (operator === '==') {
this.queryBuilder.andWhere('user.username = :username', { username: value });
} else {
this.queryBuilder.andWhere('user.username LIKE :username', { username: value });
}
return this;
}
whereEmail(value: string, operator: '==' | 'LIKE' = '=='): this {
if (operator === '==') {
this.queryBuilder.andWhere('user.email = :email', { email: value });
} else {
this.queryBuilder.andWhere('user.email LIKE :email', { email: value });
}
return this;
}
whereStatus(value: string | string[], operator: '==' | 'IN' = '=='): this {
if (operator === '==') {
this.queryBuilder.andWhere('user.status = :status', { status: value });
} else {
this.queryBuilder.andWhere('user.status IN (:...statuses)', { statuses: value });
}
return this;
}
whereCreatedAt(
value: Date | { start: Date; end: Date },
operator: '>=' | '<=' | 'BETWEEN' = '>='
): this {
if (operator === 'BETWEEN' && typeof value === 'object' && 'start' in value) {
this.queryBuilder.andWhere(
'user.createdAt BETWEEN :start AND :end',
{ start: value.start, end: value.end }
);
} else if (operator === '>=') {
this.queryBuilder.andWhere('user.createdAt >= :createdAt', { createdAt: value });
} else {
this.queryBuilder.andWhere('user.createdAt <= :createdAt', { createdAt: value });
}
return this;
}
orderBy(field: 'username' | 'email' | 'createdAt' | 'lastLoginAt', direction: 'ASC' | 'DESC' = 'ASC'): this {
this.queryBuilder.orderBy(`user.${field}`, direction);
return this;
}
paginate(page: number, pageSize: number): this {
this.queryBuilder.skip((page - 1) * pageSize).take(pageSize);
return this;
}
async execute(): Promise<User[]> {
return this.queryBuilder.getMany();
}
async executeWithCount(): Promise<[User[], number]> {
return this.queryBuilder.getManyAndCount();
}
}
// Usage:
const results = await new UserSearchBuilder(userRepository)
.whereStatus(['ACTIVE', 'SUSPENDED'], 'IN')
.whereCreatedAt({ start: new Date('2024-01-01'), end: new Date() }, 'BETWEEN')
.orderBy('createdAt', 'DESC')
.paginate(1, 20)
.executeWithCount();
Best Practices
- Name queries semantically: Use business-focused names (e.g.,
findActiveUsers) rather than technical names (e.g.,query1) - Limit complex joins: Consider materialized views for frequently-executed complex joins
- Use cursor pagination for large datasets: Cursor-based pagination performs better than offset-based for deep pagination
- Prefer query builders for dynamic filters: Use named queries for fixed queries, query builders for dynamic composition
- Index supporting fields: Ensure fields used in WHERE clauses and ORDER BY have appropriate access patterns defined
Generator Note: Query generation varies by framework. JPA uses JPQL/HQL, SQLAlchemy uses the Core/ORM API, TypeORM uses QueryBuilder, Entity Framework uses LINQ, and Core Data uses NSPredicate. The Capacitor generator translates the universal query syntax to the idioms of each platform. See Generator Guide for platform-specific examples and limitations.
Repositories and Data Access Patterns
Repositories define the public API surface of the generated data access layer. They group CRUD operations and custom queries for an entity, providing a type-safe interface for persistence operations.
Repository Declaration
A repository is declared using the repository construct and is associated with a specific entity:
namespace com.acme.data.users
/// Primary data access interface for User entities.
/// Provides CRUD operations and domain-specific queries.
repository UserRepository for User {
// Repository methods defined here
}
Auto-Generated CRUD Operations
Every repository automatically includes standard CRUD operations without explicit declaration:
create(input): Create a new entityfindById(id): Find entity by primary keyfindAll(pagination?): Find all entities (optionally paginated)update(id, input): Update an existing entitydelete(id): Delete an entity by primary keyexists(id): Check if entity exists by primary keycount(): Count total number of entities
These operations can be suppressed if not needed:
@suppress(operations: ["delete"])
repository UserRepository for User {
// delete() operation will not be generated
}
Custom Query Methods
Define custom query methods using the @query annotation:
repository UserRepository for User {
/// Find a user by their unique username.
/// Returns null if no user exists with the given username.
@query
findByUsername(username: String): User?
/// Find a user by their verified email address.
@query
findByEmail(email: Email): User?
/// Find all users with the given status, with pagination.
@query
findByStatus(status: UserStatus, page: Pageable): Page<User>
/// Find users created within a date range.
@query
findByCreatedAtBetween(start: Timestamp, end: Timestamp): List<User>
/// Check if a username is already taken.
@query
existsByUsername(username: String): Boolean
}
Generator Note: Query methods are generated using framework-specific patterns:
- Spring Data (Java): Generates interface methods with proper query derivation or
@Queryannotations - Prisma (TypeScript): Generates
findMany,findUnique,findFirstwith correct where clauses - SQLAlchemy (Python): Generates class methods with session queries
- Entity Framework (C#): Generates LINQ query methods
- GORM (Go): Generates query methods with proper where clauses
Custom Mutation Methods
Define custom mutation methods using the @mutation annotation:
repository UserRepository for User {
/// Deactivate a user account. Sets status to INACTIVE and records timestamp.
@mutation
deactivate(userId: UUID): User
/// Reactivate a previously deactivated account.
@mutation
reactivate(userId: UUID): User
/// Bulk import users from an external system.
/// Returns a result with success/failure counts.
@mutation
bulkImport(users: List<CreateUserInput>): BulkResult<User>
/// Update the last login timestamp for a user.
@mutation
recordLogin(userId: UUID, loginTime: Timestamp): User
}
Aggregation Methods
Define aggregation methods using the @aggregate annotation:
repository UserRepository for User {
/// Count users grouped by status.
@aggregate
countByStatus(): Map<UserStatus, Long>
/// Get total number of active users.
@aggregate
countActive(): Long
/// Get user statistics by status.
@aggregate
getUserStats(): List<UserStatusStats>
}
/// Supporting shape for aggregation results
shape UserStatusStats {
status: UserStatus,
count: Long,
percentOfTotal: Double
}
Repository Traits
Repositories can be annotated with behavioral traits that modify how generated code operates:
/// Cached repository with read-through caching.
@cached(ttl: "5m", strategy: "read-through")
/// All read and write operations are audited.
@audited(operations: ["read", "write"])
/// Soft-delete aware — excludes soft-deleted records by default.
@softDeleteAware
repository UserRepository for User {
// All methods inherit these behaviors
}
Available repository traits:
| Trait | Description | Parameters |
|---|---|---|
@cached | Enable caching for repository operations | ttl: Cache duration (e.g., "5m", "1h")strategy: "read-through", "write-through", "write-behind" |
@audited | Log all repository operations | operations: List of operations to audit (e.g., ["read", "write", "delete"]) |
@softDeleteAware | Automatically filter out soft-deleted entities | None |
@transactional | Wrap all operations in transactions | isolation: Transaction isolation level (e.g., "read_committed") |
@validated | Apply bean validation to inputs | None |
@monitored | Add metrics and monitoring | prefix: Metrics prefix for monitoring system |
Generator Note: Repository traits generate different implementations per framework:
- @cached: Spring Cache, Redis clients, or in-memory caching
- @audited: JPA entity listeners, SQLAlchemy events, or custom interceptors
- @softDeleteAware: WHERE clause filters or query interceptors
- @transactional: Spring
@Transactional, SQLAlchemy sessions, Prisma$transaction
Repository with Input/Output DTOs
Repositories can specify input and output types for their methods (see Input and Output Shapes for details):
repository UserRepository for User {
// Create accepts CreateUserInput, returns UserProfile
@create(input: CreateUserInput, output: UserProfile)
create(user: CreateUserInput): UserProfile
// Update accepts partial UpdateUserInput
@update(input: UpdateUserInput, output: UserProfile)
update(userId: UUID, changes: UpdateUserInput): UserProfile
// Finders return output shapes
@query(output: UserSummary)
findByStatus(status: UserStatus, page: Pageable): Page<UserSummary>
// Detail queries return full profile
@query(output: UserProfile)
findById(userId: UUID): UserProfile?
}
Transaction Boundaries
Define explicit transaction boundaries for complex operations:
repository AccountRepository for Account {
/// Transfer funds between two accounts atomically.
@transaction(isolation: "serializable")
@mutation
transferFunds(
fromAccountId: UUID,
toAccountId: UUID,
amount: Decimal,
reference: String
): TransferResult
}
shape TransferResult {
transactionId: UUID,
fromBalance: Decimal,
toBalance: Decimal,
timestamp: Timestamp
}
Generator Note: Transaction boundaries generate framework-specific transaction management:
- Spring:
@Transactionalwith specified isolation level - SQLAlchemy: Session transaction context manager
- Prisma:
$transactionAPI - Entity Framework:
TransactionScopeorBeginTransaction
Best Practices
- Single Responsibility: One repository per entity
- Semantic Naming: Name methods after business operations (
deactivate,recordLogin) not technical operations (updateStatus,setTimestamp) - Return Types: Use specific output DTOs for read operations to control what data is exposed
- Pagination: Always paginate list queries that could return large result sets
- Transactions: Be explicit about transaction boundaries for multi-step operations
- Documentation: Document side effects, preconditions, and postconditions for custom operations
- Error Handling: Let the framework handle constraint violations and not-found cases
- Testing: Repositories are the ideal boundary for integration testing
Generator Note: Repositories are the primary interface that application code uses to interact with the database. All generated repository code includes comprehensive documentation, type safety, and framework-specific best practices. See Generator Guide for implementation details per target framework.
Input and Output Shapes (DTOs)
Input and Output shapes (Data Transfer Objects) provide clean API boundaries between your persistence layer and application code. They control what data is accepted for writes and what data is returned for reads, preventing exposure of internal fields and providing type-safe interfaces.
Input Shapes for Write Operations
Input shapes define what data is required when creating or updating entities. They automatically exclude auto-generated or system-managed fields:
namespace com.acme.data.users
/// Input for creating a new user.
/// Excludes auto-generated fields (userId, createdAt, status).
input CreateUserInput for User {
include: [username, email, phone]
// userId, createdAt, status are excluded — they're auto-generated
}
Generated types vary by language:
Java:
public class CreateUserInput {
@NotNull
private String username;
@NotNull @Email
private String email;
private String phone;
// Getters, setters, builder pattern
}
TypeScript:
export interface CreateUserInput {
username: string;
email: string;
phone?: string;
}
Python:
class CreateUserInput(BaseModel):
username: str
email: EmailStr
phone: Optional[str] = None
Update Inputs with Partial Fields
Update inputs typically make all fields optional to support partial updates:
/// Input for updating an existing user.
/// All fields are optional (partial update semantics).
input UpdateUserInput for User {
include: [email, phone, status],
allOptional: true // Every field becomes nullable for partial updates
}
Generated TypeScript:
export interface UpdateUserInput {
email?: string;
phone?: string;
status?: UserStatus;
}
Field Overrides in Inputs
Override field validation or types in input shapes:
input CreateProductInput for Product {
include: [name, description, price, category],
/// Override: price validation is relaxed in input (final validation in entity)
@override
price: Decimal // Different validation rules than entity
/// Override: category accepts string, converted to enum
@override
category: String // Will be validated and converted to ProductCategory enum
}
Output Shapes for Read Operations
Output shapes control what data is returned, providing different views for different use cases:
/// Compact user representation for list views and search results.
output UserSummary from User {
include: [userId, username, email, status, createdAt],
exclude: [ssn, phone] // Never expose sensitive fields in summaries
}
/// Full user profile with computed fields.
output UserProfile from User {
includeAll: true,
exclude: [ssn], // Exclude only the most sensitive field
/// Additional computed field not in the entity
@computed(expression: "createdAt < now() - 365d")
isVeteranUser: Boolean
}
/// Minimal reference for embedding in other responses.
output UserRef from User {
include: [userId, username]
}
Generated Java DTOs:
// UserSummary
public class UserSummary {
private UUID userId;
private String username;
private String email;
private UserStatus status;
private Instant createdAt;
// Getters only (immutable DTO)
}
// UserProfile
public class UserProfile {
private UUID userId;
private String username;
private String email;
private String phone; // ssn excluded
private UserStatus status;
private Instant createdAt;
private Boolean isVeteranUser; // computed
// Getters only
}
Include/Exclude Patterns
Multiple patterns are supported for field selection:
// Explicit include only
output MinimalUser from User {
include: [userId, username]
}
// Include all except specific fields
output PublicUser from User {
includeAll: true,
exclude: [ssn, internalNotes, fraudScore]
}
// Include with additions
output EnrichedUser from User {
includeAll: true,
// Add computed fields not in entity
@computed(expression: "posts.length")
postCount: Integer,
@computed(expression: "lastLoginAt > now() - 7d")
isRecentlyActive: Boolean
}
Nested DTOs
Output shapes can reference other output shapes:
output OrderSummary from Order {
include: [orderId, total, status, createdAt],
// Reference another output shape
customer: UserRef // Embeds minimal user reference
}
output OrderDetail from Order {
includeAll: true,
customer: UserProfile, // Full customer profile
items: List<OrderItemDetail> // List of detailed line items
}
output OrderItemDetail from OrderItem {
include: [productName, quantity, price, subtotal]
}
DTO Usage in Repositories
Input and output shapes integrate seamlessly with repository methods:
repository UserRepository for User {
// Create accepts input, returns output
@create(input: CreateUserInput, output: UserProfile)
create(user: CreateUserInput): UserProfile
// Update accepts partial input
@update(input: UpdateUserInput, output: UserProfile)
update(userId: UUID, changes: UpdateUserInput): UserProfile
// Queries can return different output shapes
@query(output: UserSummary)
findAll(page: Pageable): Page<UserSummary>
@query(output: UserProfile)
findById(userId: UUID): UserProfile?
@query(output: UserRef)
searchByUsername(username: String): List<UserRef>
}
DTO Mapping Generation
Generators produce mapping code between entities and DTOs:
Java (MapStruct):
@Mapper(componentModel = "spring")
public interface UserMapper {
UserProfile toProfile(User user);
UserSummary toSummary(User user);
User fromCreateInput(CreateUserInput input);
@Mapping(target = "userId", ignore = true)
@Mapping(target = "createdAt", ignore = true)
void updateFromInput(@MappingTarget User user, UpdateUserInput input);
}
TypeScript (manual or ts-transformer):
export class UserMapper {
static toProfile(user: User): UserProfile {
return {
userId: user.userId,
username: user.username,
email: user.email,
phone: user.phone,
status: user.status,
createdAt: user.createdAt,
isVeteranUser: isOlderThan365Days(user.createdAt)
};
}
static toSummary(user: User): UserSummary {
return {
userId: user.userId,
username: user.username,
email: user.email,
status: user.status,
createdAt: user.createdAt
};
}
}
Python (Pydantic):
class UserMapper:
@staticmethod
def to_profile(user: User) -> UserProfile:
return UserProfile(
userId=user.user_id,
username=user.username,
email=user.email,
phone=user.phone,
status=user.status,
createdAt=user.created_at,
isVeteranUser=is_veteran_user(user.created_at)
)
Validation in DTOs
Input DTOs inherit validation rules from the entity but can override them:
entity User {
@identity userId: UUID,
@length(min: 3, max: 50)
@pattern(regex: "^[a-zA-Z0-9_]+$")
username: String,
@pii
email: Email
}
input CreateUserInput for User {
include: [username, email],
/// Override: relax length constraint during input (will be validated in entity)
@override
@length(min: 2, max: 100) // More lenient
username: String
}
Generator Note: Validation annotations are translated to framework-specific validators:
- Java: Bean Validation annotations (
@NotNull,@Email,@Size,@Pattern) - TypeScript: class-validator decorators
- Python: Pydantic validators
- C#: Data Annotations
Best Practices
- Separate concerns: Use different output shapes for lists (summary) vs. details (profile)
- Never expose PII in summaries: Mark sensitive fields with
@piiand exclude from list views - Immutable outputs: Output DTOs should be immutable (read-only)
- Partial updates: Use
allOptional: truefor update inputs - Computed fields in outputs: Add derived fields that don't need to be stored
- Minimal references: Use minimal output shapes (like
UserRef) when embedding in other DTOs - Consistent naming: Use suffixes like
Input,Summary,Profile,Detail,Ref
Generator Note: DTO generation is a core feature of all Capacitor generators. The mapping between entities and DTOs is type-safe and validated at generation time. See Generator Guide for language-specific implementation patterns.
Services and Bounded Contexts
The service construct defines the top-level packaging unit in Capacitor. It groups related entities, repositories, shapes, and enums into a cohesive bounded context that becomes a publishable library artifact.
Service Declaration
A service defines the public API surface and versioning for a logical domain:
namespace com.acme.data.users
/// User domain data access layer.
///
/// This service provides all persistence operations for the user domain,
/// including user accounts, preferences, and sessions.
service UserDataService {
version: "2.1.0",
// Entities owned by this service
entities: [
User,
UserPreferences,
Session,
LoginHistory
],
// Repositories exposed as the public API
repositories: [
UserRepository,
SessionRepository
],
// Shared shapes used in inputs/outputs
shapes: [
Address,
ContactInfo
],
// Enums
enums: [
UserStatus,
SessionType
],
// Streams/CDC (optional)
streams: [
UserChanges,
SessionEvents
]
}
Service as Packaging Unit
The service construct maps directly to language-specific package structures:
Java (Maven):
<dependency>
<groupId>com.acme.data</groupId>
<artifactId>user-service-dal</artifactId>
<version>2.1.0</version>
</dependency>
TypeScript (npm):
{
"name": "@acme/user-service-dal",
"version": "2.1.0"
}
Python (PyPI):
[project]
name = "acme-user-service-dal"
version = "2.1.0"
Go (module):
module github.com/acme/user-service-dal
go 1.21
require (
// dependencies
)
Public vs. Private Types
Only types explicitly listed in the service definition are included in the generated package's public API:
namespace com.acme.data.users
// This entity is private (not in service definition)
entity InternalAuditLog {
@identity
id: UUID,
action: String,
timestamp: Timestamp
}
// This entity is public (listed in service)
entity User {
@identity
userId: UUID,
username: String,
email: Email
}
service UserDataService {
version: "1.0.0",
// Only User is public
entities: [User],
repositories: [UserRepository]
// InternalAuditLog is not exposed
}
Generator Note: Private entities and types are not exported in the generated package. They exist only within the service's internal implementation and cannot be referenced by consumers.
Service Versioning
Services follow semantic versioning (MAJOR.MINOR.PATCH):
service UserDataService {
version: "2.1.0", // Breaking.Feature.Fix
entities: [User, UserPreferences],
repositories: [UserRepository]
}
Version constraints in consumers:
{
"dependencies": {
"com.acme.data.users": {
"version": "^2.1.0", // Compatible with 2.x.x (< 3.0.0)
"repository": "https://repo.acme.com/capacitor"
}
}
}
Service Dependencies
Services can depend on other services:
namespace com.acme.data.orders
use com.acme.data.users#User
use com.acme.data.users#UserRef
use com.acme.data.products#Product
use com.acme.data.common#Address
service OrderDataService {
version: "1.5.0",
entities: [Order, OrderItem, Payment],
repositories: [OrderRepository, PaymentRepository],
// Dependencies are implicit via 'use' imports
// Generators ensure proper dependency resolution
}
Generated package dependencies:
Java (pom.xml):
<dependencies>
<dependency>
<groupId>com.acme.data</groupId>
<artifactId>user-service-dal</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.acme.data</groupId>
<artifactId>product-service-dal</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
TypeScript (package.json):
{
"dependencies": {
"@acme/user-service-dal": "^2.1.0",
"@acme/product-service-dal": "^1.0.0"
}
}
Multi-Service Projects
Large projects can define multiple services in separate namespaces:
models/
├── com/acme/data/
│ ├── common/
│ │ ├── types.capacitor
│ │ └── service.capacitor // CommonDataService
│ ├── users/
│ │ ├── user.capacitor
│ │ ├── repositories.capacitor
│ │ └── service.capacitor // UserDataService
│ ├── orders/
│ │ ├── order.capacitor
│ │ ├── repositories.capacitor
│ │ └── service.capacitor // OrderDataService
│ └── products/
│ ├── product.capacitor
│ ├── repositories.capacitor
│ └── service.capacitor // ProductDataService
Build configuration generates multiple artifacts:
{
"generators": {
"user-service": {
"generator": "capacitor:java-jpa",
"service": "UserDataService",
"output": "./generated/user-service",
"artifact": {
"artifactId": "user-service-dal",
"version": "2.1.0"
}
},
"order-service": {
"generator": "capacitor:java-jpa",
"service": "OrderDataService",
"output": "./generated/order-service",
"artifact": {
"artifactId": "order-service-dal",
"version": "1.5.0"
}
}
}
}
Service Metadata
Services support additional metadata for documentation and discovery:
@documentation(
summary: "User domain data access layer",
description: """
Provides type-safe persistence operations for user management,
including accounts, preferences, sessions, and authentication history.
""",
authors: ["data-platform-team@acme.com"],
tags: ["user-management", "authentication"]
)
service UserDataService {
version: "2.1.0",
entities: [User, UserPreferences, Session],
repositories: [UserRepository, SessionRepository]
}
Service as Bounded Context
Services align with Domain-Driven Design (DDD) bounded contexts:
// User bounded context
namespace com.acme.domain.user
service UserDomainService {
version: "1.0.0",
entities: [User, UserProfile, UserSettings],
repositories: [UserRepository]
}
// Order bounded context
namespace com.acme.domain.order
use com.acme.domain.user#User as UserRef // Reference, not ownership
service OrderDomainService {
version: "1.0.0",
entities: [Order, OrderItem, Invoice],
repositories: [OrderRepository, InvoiceRepository]
// References User but doesn't own it
}
Best practices:
- Each service owns its entities exclusively
- Cross-service references use minimal DTOs (like
UserRef) - Avoid circular dependencies between services
- Use event streams for cross-service communication
Best Practices
- One service per bounded context: Align services with business domains
- Clear ownership: Each entity belongs to exactly one service
- Minimal public API: Only expose what consumers need
- Semantic versioning: Increment major version for breaking changes
- Documentation: Document the service's purpose, scope, and usage
- Stable interfaces: Once published, maintain backward compatibility within major versions
- Internal types: Use
@internaltrait for implementation details not in the service - Dependency management: Keep service dependencies minimal and well-defined
Generator Note: Services are the packaging and publishing unit for Capacitor-generated code. Each service generates a complete, self-contained library with proper dependency management, documentation, and semantic versioning. See Build Configuration for artifact packaging details and Generator Guide for implementation patterns.
Computed Fields
Computed fields derive their value from other fields using an expression. They are calculated, not stored (unless explicitly specified).
entity Order {
@identity
orderId: UUID,
subtotal: Decimal,
taxRate: Decimal,
shippingCost: Decimal,
@computed(expression: "subtotal * (1 + taxRate)")
totalWithTax: Decimal,
@computed(expression: "subtotal * (1 + taxRate) + shippingCost")
grandTotal: Decimal
}
Stored computed fields (calculated once on write):
entity User {
@identity
userId: UUID,
firstName: String,
lastName: String,
@computed(expression: "firstName + ' ' + lastName", stored: true)
fullName: String
}
Computed with database functions:
entity Event {
@identity
eventId: UUID,
startTime: Timestamp,
endTime: Timestamp,
@computed(expression: "endTime - startTime")
duration: Integer // Duration in seconds
}
Generator Note: Computed fields map to generated columns (SQL), virtual fields (MongoDB), or application-level calculations. See Generator Guide.
Sequences and Identity Generation
Sequences
Sequences provide auto-incrementing numeric identifiers.
sequence InvoiceNumberSequence {
start: 1000,
increment: 1,
cache: 20
}
entity Invoice {
@identity
@sequence(InvoiceNumberSequence)
invoiceNumber: Integer,
customerId: UUID,
amount: Decimal,
createdAt: Timestamp
}
Multiple sequences:
sequence OrderNumberSequence {
start: 10000,
increment: 1,
prefix: "ORD"
}
sequence ReturnNumberSequence {
start: 5000,
increment: 1,
prefix: "RET"
}
Auto-generated Identifiers
entity User {
@identity
@default(value: "uuid()")
userId: UUID,
username: String
}
entity Log {
@identity
@default(value: "ulid()") // Time-sortable UUID
logId: String,
message: String,
timestamp: Timestamp
}
Available generators:
uuid(): UUID v4ulid(): Universally Unique Lexicographically Sortable Identifiercuid(): Collision-resistant ID optimized for horizontal scalingtimestamp(): Current timestampsequence(name): Named sequence
Generator Note: Sequences map to native sequences (PostgreSQL), AUTO_INCREMENT (MySQL), or application-level generators for databases without native support. See Generator Guide.
Advanced Language Features
Capacitor provides advanced features for cross-field validation, transactional operations, test data generation, and access control. These features enable sophisticated data modeling patterns while maintaining portability across databases.
Entity-Level Invariants
Invariants are cross-field validation rules that ensure data consistency beyond single-field constraints. They validate relationships between multiple fields within an entity.
Basic Invariants
entity DateRange {
@identity
id: UUID,
startDate: Date,
endDate: Date,
@invariant(
condition: "endDate > startDate",
message: "End date must be after start date"
)
@invariant(
condition: "endDate - startDate <= 365",
message: "Range cannot exceed one year"
)
}
Exclusive Field Validation
entity Discount {
@identity
id: UUID,
discountPercent: Decimal?,
discountAmount: Decimal?,
@invariant(
condition: "discountPercent != null OR discountAmount != null",
message: "Either percentage or fixed amount discount must be specified"
)
@invariant(
condition: "NOT (discountPercent != null AND discountAmount != null)",
message: "Cannot specify both percentage and fixed amount discount"
)
}
Complex Business Rules
entity Shipment {
@identity
shipmentId: UUID,
weight: Decimal, // in kg
volume: Decimal, // in m³
hazardousMaterial: Boolean,
insuranceRequired: Boolean,
@invariant(
condition: "(weight > 1000 OR volume > 10) IMPLIES insuranceRequired",
message: "Insurance required for shipments over 1000kg or 10m³"
)
@invariant(
condition: "hazardousMaterial IMPLIES insuranceRequired",
message: "Insurance required for hazardous materials"
)
}
Generator Note: Invariant implementation varies by target:
- SQL databases:
CHECKconstraints where expressible, stored procedures for complex logic - JPA: Bean Validation
@AssertTruemethods or custom validators - SQLAlchemy:
@validatesdecorators orCheckConstraint - TypeORM: class-validator decorators
- Entity Framework:
IValidatableObjectimplementation - NoSQL databases: Validation in generated SDK code
Transaction Modeling
The transaction construct defines atomic operations that span multiple entities or involve complex business logic.
Simple Transaction
namespace com.acme.data.banking
/// Transfer funds between accounts atomically.
transaction TransferFunds {
entities: [Account, TransactionLog],
isolation: "read_committed", // or serializable, repeatable_read
params: {
fromAccountId: UUID,
toAccountId: UUID,
amount: Decimal,
reference: String
},
steps: [
"debit fromAccount by amount",
"credit toAccount by amount",
"create TransactionLog entry"
],
rollbackOn: ["InsufficientFundsException", "AccountFrozenException"]
}
Generated implementations:
Java (Spring):
@Service
public class BankingService {
@Transactional(
isolation = Isolation.READ_COMMITTED,
rollbackFor = {InsufficientFundsException.class, AccountFrozenException.class}
)
public TransactionLog transferFunds(
UUID fromAccountId,
UUID toAccountId,
BigDecimal amount,
String reference
) {
// Generated implementation
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new AccountNotFoundException(fromAccountId));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new AccountNotFoundException(toAccountId));
// Business logic
fromAccount.debit(amount); // May throw InsufficientFundsException
toAccount.credit(amount);
// Save accounts
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
// Log transaction
TransactionLog log = TransactionLog.builder()
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.amount(amount)
.reference(reference)
.timestamp(Instant.now())
.build();
return transactionLogRepository.save(log);
}
}
Python (SQLAlchemy):
class BankingService:
def transfer_funds(
self,
from_account_id: UUID,
to_account_id: UUID,
amount: Decimal,
reference: str
) -> TransactionLog:
with db.session.begin(): # Automatic rollback on exception
from_account = Account.query.get_or_404(from_account_id)
to_account = Account.query.get_or_404(to_account_id)
from_account.debit(amount) # May raise InsufficientFundsException
to_account.credit(amount)
log = TransactionLog(
from_account_id=from_account_id,
to_account_id=to_account_id,
amount=amount,
reference=reference,
timestamp=datetime.utcnow()
)
db.session.add(log)
return log
TypeScript (Prisma):
export class BankingService {
async transferFunds(
fromAccountId: string,
toAccountId: string,
amount: number,
reference: string
): Promise<TransactionLog> {
return await prisma.$transaction(async (tx) => {
const fromAccount = await tx.account.findUniqueOrThrow({
where: { id: fromAccountId }
});
const toAccount = await tx.account.findUniqueOrThrow({
where: { id: toAccountId }
});
// Business logic with validation
if (fromAccount.balance < amount) {
throw new InsufficientFundsException();
}
await tx.account.update({
where: { id: fromAccountId },
data: { balance: fromAccount.balance - amount }
});
await tx.account.update({
where: { id: toAccountId },
data: { balance: toAccount.balance + amount }
});
return await tx.transactionLog.create({
data: {
fromAccountId,
toAccountId,
amount,
reference,
timestamp: new Date()
}
});
}, {
isolationLevel: 'ReadCommitted'
});
}
}
Transaction Options
| Option | Values | Description |
|---|---|---|
isolation | read_uncommitted, read_committed, repeatable_read, serializable | Transaction isolation level |
timeout | Duration (e.g., "30s", "5m") | Maximum transaction duration |
readOnly | Boolean | Transaction is read-only (optimization) |
rollbackOn | List of exception types | Exceptions that trigger rollback |
noRollbackOn | List of exception types | Exceptions that don't trigger rollback |
Generator Note: Transaction definitions generate framework-specific transactional service methods with proper isolation, rollback rules, and error handling.
Test Fixtures
Fixtures define reusable test data that generators use to create test helpers, seed scripts, and factory functions.
Basic Fixtures
namespace com.acme.data.users
/// Standard test data for user-related tests.
fixture UserFixtures for User {
activeUser: {
userId: "550e8400-e29b-41d4-a716-446655440000",
username: "alice",
email: "alice@example.com",
status: UserStatus.ACTIVE,
createdAt: "2024-01-15T10:00:00Z"
},
suspendedUser: {
userId: "550e8400-e29b-41d4-a716-446655440001",
username: "bob",
email: "bob@example.com",
status: UserStatus.SUSPENDED,
createdAt: "2024-03-20T14:30:00Z"
},
newUser: {
userId: "550e8400-e29b-41d4-a716-446655440002",
username: "charlie",
email: "charlie@example.com",
status: UserStatus.PENDING,
createdAt: "2024-06-01T09:00:00Z"
}
}
Generated test helpers:
Java (JUnit + Spring):
public class UserFixtures {
public static User activeUser() {
return User.builder()
.userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
.username("alice")
.email("alice@example.com")
.status(UserStatus.ACTIVE)
.createdAt(Instant.parse("2024-01-15T10:00:00Z"))
.build();
}
public static User suspendedUser() {
return User.builder()
.userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440001"))
.username("bob")
.email("bob@example.com")
.status(UserStatus.SUSPENDED)
.createdAt(Instant.parse("2024-03-20T14:30:00Z"))
.build();
}
public static User newUser() {
return User.builder()
.userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440002"))
.username("charlie")
.email("charlie@example.com")
.status(UserStatus.PENDING)
.createdAt(Instant.parse("2024-06-01T09:00:00Z"))
.build();
}
}
// SQL seed script also generated:
// INSERT INTO users (user_id, username, email, status, created_at)
// VALUES ('550e8400-e29b-41d4-a716-446655440000', 'alice', 'alice@example.com', 'ACTIVE', '2024-01-15 10:00:00');
TypeScript (Jest):
export const UserFixtures = {
activeUser: (): User => ({
userId: "550e8400-e29b-41d4-a716-446655440000",
username: "alice",
email: "alice@example.com",
status: UserStatus.ACTIVE,
createdAt: new Date("2024-01-15T10:00:00Z")
}),
suspendedUser: (): User => ({
userId: "550e8400-e29b-41d4-a716-446655440001",
username: "bob",
email: "bob@example.com",
status: UserStatus.SUSPENDED,
createdAt: new Date("2024-03-20T14:30:00Z")
}),
newUser: (): User => ({
userId: "550e8400-e29b-41d4-a716-446655440002",
username: "charlie",
email: "charlie@example.com",
status: UserStatus.PENDING,
createdAt: new Date("2024-06-01T09:00:00Z")
})
};
Python (pytest):
@pytest.fixture
def active_user():
return User(
user_id=UUID("550e8400-e29b-41d4-a716-446655440000"),
username="alice",
email="alice@example.com",
status=UserStatus.ACTIVE,
created_at=datetime(2024, 1, 15, 10, 0, 0, tzinfo=timezone.utc)
)
@pytest.fixture
def suspended_user():
return User(
user_id=UUID("550e8400-e29b-41d4-a716-446655440001"),
username="bob",
email="bob@example.com",
status=UserStatus.SUSPENDED,
created_at=datetime(2024, 3, 20, 14, 30, 0, tzinfo=timezone.utc)
)
Parameterized Fixtures
fixture OrderFixtures for Order {
// Template with parameters
template orderTemplate(customerId: UUID, total: Decimal): {
orderId: "uuid()", // Generate UUID
customerId: customerId,
total: total,
status: OrderStatus.PENDING,
createdAt: "now()" // Use current timestamp
},
// Predefined instances
smallOrder: orderTemplate(
customerId: "550e8400-e29b-41d4-a716-446655440000",
total: 49.99
),
largeOrder: orderTemplate(
customerId: "550e8400-e29b-41d4-a716-446655440001",
total: 999.99
)
}
Internal Markers
The @internal trait marks fields and entities as internal implementation details that should not be exposed in public APIs.
Internal Fields
entity User {
@identity
userId: UUID,
username: String,
email: Email,
@internal
/// Internal fraud score, never exposed to external consumers
fraudScore: Double?,
@internal
/// Admin-only notes
internalNotes: String?
}
Effect: Internal fields are:
- Excluded from public DTOs and output shapes
- Excluded from generated API documentation
- Excluded from projections (filtered model views)
- Marked as private/internal in generated code
Internal Entities
@internal
entity InternalAuditLog {
@identity
id: UUID,
userId: UUID,
action: String,
ipAddress: String,
timestamp: Timestamp,
metadata: Document
}
service UserDataService {
version: "1.0.0",
entities: [User, UserPreferences],
repositories: [UserRepository]
// InternalAuditLog is not listed - it's internal only
}
Effect: Internal entities are:
- Not included in service public API
- Not exported from generated packages
- Only accessible within the service implementation
- Not included in documentation generation
Readonly Trait
The @readonly trait marks fields that cannot be modified after entity creation:
entity User {
@identity
@readonly
userId: UUID,
username: String,
@readonly
createdAt: Timestamp,
@readonly
createdBy: UUID?
}
Generated code includes immutability enforcement:
- JPA:
@Column(updatable = false) - SQLAlchemy:
onupdate=None - TypeORM:
@Column({ update: false }) - Prisma: Excluded from update inputs
Best Practices
Invariants:
- Keep invariant expressions simple and testable
- Document business rules clearly in the message
- Prefer database-level CHECK constraints for simple rules
- Use application-level validation for complex cross-entity logic
Transactions:
- Define transactions for multi-step operations that must be atomic
- Use appropriate isolation levels (default is usually sufficient)
- Keep transactions short-lived to avoid lock contention
- Handle rollback scenarios explicitly
Fixtures:
- Create fixtures for common test scenarios
- Use realistic data that represents actual use cases
- Include edge cases (empty, maximum, null values)
- Keep fixture data consistent with constraints
Internal Markers:
- Mark truly internal fields/entities, not just sensitive data (use
@piifor that) - Document why something is internal
- Review internal markers before major version releases
Generator Note: Advanced features demonstrate Capacitor's ability to express complex data semantics that generators implement appropriately for each target platform. These features maintain portability while providing sophisticated data modeling capabilities. See Generator Guide for platform-specific implementations.
Schema Evolution
Capacitor treats time as a first-class variable, allowing for safe migrations without downtime.
Deprecation
entity Profile {
@identity
id: UUID,
@deprecated(message: "Use bio_rich_text instead", removeIn: "3.0")
bio_text: String?,
@since("2.0")
bio_rich_text: Document?
}
Field Renaming
entity User {
@identity
userId: UUID,
@renamed(from: "user_name")
username: String,
@renamed(from: "mail")
email: Email
}
Version Tracking
entity Product {
@identity
sku: String,
name: String,
price: Decimal,
@since("1.0")
inventory: Integer,
@since("1.5")
imageUrls: List<URL>?,
@since("2.0")
@optional
variants: List<ProductVariant>?
}
Migration Strategies
@migration(strategy: "dual-write")
entity Order {
@identity
orderId: UUID,
// Old field - read during migration
@deprecated(removeIn: "2.0")
status_string: String?,
// New field - dual write during migration
@since("1.5")
status: OrderStatus
}
Migration strategies:
dual-write: Write to both old and new fieldsbackfill: Populate new field from old field in batcheslazy: Migrate on read/writecutover: Direct switch at specific time
Data Governance and Security
Security policies are defined at the model level to ensure enforcement regardless of access path.
Sensitive Data Markers
entity User {
@identity
userId: UUID,
username: String,
@pii
email: Email,
@pii
@encrypted
ssn: String,
@pii
phone: Phone?
}
Data Retention
@ttl(duration: "90d", field: "expiresAt")
entity Session {
@identity
sessionId: String,
userId: UUID,
createdAt: Timestamp,
@computed(expression: "createdAt + 90d")
expiresAt: Timestamp
}
Soft deletes:
@softDelete(field: "deletedAt")
entity Document {
@identity
docId: UUID,
title: String,
content: String,
deletedAt: Timestamp? // Null = active
}
Immutability
@immutable
entity AuditLog {
@identity
id: UUID,
@readonly
userId: UUID,
@readonly
action: String,
@readonly
timestamp: Timestamp,
@readonly
details: Document
}
Audit Trails
@audit(track: ["insert", "update", "delete"])
entity Transaction {
@identity
txId: UUID,
amount: Decimal,
@pii
accountNumber: String,
status: String
}
Multi-tenancy
@multiTenant(strategy: "row-level", key: "tenantId")
entity Account {
@identity
accountId: UUID,
@tenant
tenantId: UUID,
name: String,
data: Document
}
Tenancy strategies:
row-level: Shared tables with tenant columnschema-level: Separate schema per tenantdatabase-level: Separate database per tenant
Reactivity and Streams
Capacitor captures data changes and can route them to external systems.
Change Data Capture (CDC)
stream UserChanges {
source: User,
destination: "kafka/topic/user-events",
events: ["insert", "update", "delete"],
format: "json"
}
With filtering:
stream HighValueOrders {
source: Order,
destination: "aws/eventbridge",
filter: "amount > 10000",
events: ["insert"],
transform: "order_to_alert"
}
Triggers
@onInsert(fn: "sendWelcomeEmail")
@onUpdate(fn: "auditStatusChange", when: "status != old.status")
entity User {
@identity
userId: UUID,
email: Email,
status: String
}
Derived Data Sync
stream SyncActiveUsers {
source: User,
destination: "materialized_view/active_users",
filter: "status == 'active'",
events: ["insert", "update", "delete"],
mode: "upsert"
}
Generator Note: Streams map to database-native CDC (PostgreSQL logical replication, MySQL binlog, DynamoDB Streams) or require external tools. See Generator Guide.
The Weave
The weave block defines deployment targets and their configurations.
Basic Weave
weave Development {
target: "postgres/15",
entities: [User, Order, Product],
config {
host: "localhost",
port: 5432,
database: "myapp_dev",
ssl: false
}
}
Multi-Environment
weave Development {
target: "postgres/15",
entities: [User, Order, Product]
config {
host: "localhost",
port: 5432,
database: "myapp_dev"
}
}
weave Production {
target: "postgres/15",
storage: "aws/rds",
entities: [User, Order, Product]
config {
host: "${env:DB_HOST}",
port: 5432,
database: "myapp_prod",
ssl: true,
encryption_at_rest: true,
backup_retention: "30d",
ha: true,
replicas: 2
}
}
Multi-Database
weave UserService {
target: "postgres/15",
entities: [User, Session],
config {
database: "users"
}
}
weave Analytics {
target: "dynamodb",
storage: "aws/dynamodb",
entities: [Event, Metric],
config {
region: "us-east-1",
billing_mode: "PAY_PER_REQUEST"
}
}
CLI and Workflow
Commands
Click any command to expand its full documentation.
capacitor check — Validate model against semantic rules and target compatibility
Validates the model against semantic rules and checks target compatibility.
capacitor check --target postgres
# Output:
# ✓ models/user.capacitor - Valid
# ⚠ models/order-summary.capacitor:5 - Materialized view requires manual refresh
# ✓ 5 entities checked
# ⚠ 1 warning
capacitor plan — Generate a deployment plan showing what will be created or changed
Generates a deployment plan showing what will be created or changed.
capacitor plan --target production
# Output:
# Plan for target: production (PostgreSQL 15)
#
# Changes:
# + CREATE TABLE users (...)
# + CREATE INDEX idx_users_email ON users(email)
# + CREATE MATERIALIZED VIEW order_summary AS ...
# ~ ALTER TABLE orders ADD COLUMN priority INTEGER
# - DROP INDEX idx_orders_old
capacitor apply — Apply planned changes to the target environment
Applies the planned changes to the target environment.
capacitor apply --target production
# With confirmation:
capacitor apply --target production --auto-approve=false
capacitor build — Generate and package SDKs defined in the build configuration
Generates all SDKs defined in the build configuration. Also supports packaging and publishing.
# Generate all configured SDKs
capacitor build
# Generate specific SDK by name
capacitor build --generator typescript-sdk
# Generate all SDKs for a target
capacitor build --target development
# Generate and package (creates .jar, .tgz, .whl, etc.)
capacitor build --package
# Generate, package, and publish to configured repositories
capacitor build --publish
# Dry-run publish (shows what would be published)
capacitor build --publish --dry-run
# Generate for a specific projection
capacitor build --generator java-jpa-sdk --projection public-api
# Generate documentation only
capacitor build --docs
Output example for --package:
✓ Generated 47 files in ./generated/java
✓ Packaged com.acme.data:user-service-dal:2.1.0.jar
✓ Generated 32 files in ./generated/typescript
✓ Packaged @acme/user-service-dal-2.1.0.tgz
Output example for --publish:
✓ Published to Maven: com.acme.data:user-service-dal:2.1.0
✓ Published to npm: @acme/user-service-dal@2.1.0
✓ Published to PyPI: acme-user-service-dal==2.1.0
capacitor generate — Generate type-safe SDKs with custom language and framework flags
Generates type-safe SDKs for application code. Can use build configuration or CLI flags.
# Using build config (recommended)
capacitor build --generator typescript-sdk
# Or override with CLI flags
capacitor generate --lang typescript --framework typeorm --output ./src/generated
# Additional examples
capacitor generate --lang java --framework jpa --output ./src/main/java/dal
capacitor generate --lang python --framework sqlalchemy --output ./app/dal
capacitor generate --lang go --framework gorm --output ./internal/dal
capacitor generate --lang csharp --framework entity-framework --output ./Dal
capacitor generate --lang swift --framework core-data --output ./Models
capacitor migrate — Manage schema migrations between versions
Manages schema migrations between versions.
# Generate migration
capacitor migrate create --name add_user_preferences
# Apply migrations
capacitor migrate up --target production
# Rollback
capacitor migrate down --target production --steps 1
capacitor lint — Run linter rules and enforce model conventions
Runs all linter rules defined in capacitor.config.yaml:
# Run all linter rules
capacitor lint
# Run linter in strict mode (warnings become errors)
capacitor lint --strict
# Run specific linter rules
capacitor lint --rules naming-conventions,require-documentation
# Output validation report
capacitor lint --output lint-report.json
# Example output:
# ✓ 5 entities validated
# ⚠ 2 warnings:
# - User entity missing documentation (line 42)
# - Product entity has 35 fields (max: 30) (line 108)
# ✓ 0 errors
Available linter rules:
require-documentation: Entities and repositories should be documentedrequire-access-patterns: Entities should define access patternsnaming-conventions: Enforce naming conventions (PascalCase, camelCase, etc.)max-entity-fields: Limit entity field countshimmed-features: Warn about features requiring shimsunsupported-features: Error on unsupported featuresperformance-hints: Performance optimization suggestions
capacitor compatibility — Show compatibility report for generators and target databases
Shows compatibility report for generators and target databases:
# Show compatibility for a specific generator
capacitor compatibility --generator java-jpa-sdk
# Show compatibility for all generators
capacitor compatibility
# Output in JSON format
capacitor compatibility --format json
# Example output:
Compatibility Report for java-jpa-sdk (JPA + PostgreSQL 15):
────────────────────────────────────────────────────────
✅ NATIVE: entities, shapes, enums, relationships, unique constraints,
check constraints, sequences, views, materialized views,
repositories, mixins, invariants
⚡ SHIMMED: soft deletes (application interceptor),
encryption (@pii - application-level),
audit trails (JPA event listeners),
multi-tenancy (Hibernate filter)
⚠️ PARTIAL: full-text search (native tsvector, but limited ranking options)
❌ UNSUPPORTED: none
Generated 47 files in ./src/main/java/com/acme/dal
capacitor validate — Validate cross-namespace dependencies and imports
Validates cross-namespace dependencies and imports:
# Validate all namespaces and imports
capacitor validate
# Validate specific namespace
capacitor validate --namespace com.acme.data.users
# Check for circular dependencies
capacitor validate --check-circular
# Example output:
# ✓ Namespace com.acme.data.users validated
# ✓ All imports resolved
# ✓ No circular dependencies detected
# ✓ 3 external dependencies validated
capacitor docs — Generate documentation in HTML, Markdown, or other formats
Generate documentation in various formats:
# Generate HTML documentation
capacitor docs --format html --output ./docs/api
# Generate Markdown documentation
capacitor docs --format markdown --output ./docs/md
# Generate all configured documentation formats
capacitor docs
# Example output:
# ✓ HTML docs → ./docs/api (12 pages, ER diagram, search index)
# ✓ Markdown docs → ./docs/md (15 files)
# ✓ Generated documentation for 5 entities, 3 repositories
Build Configuration
Capacitor uses a declarative build configuration file (similar to Smithy's smithy-build.json) that defines both database targets and SDK generation.
Important: The build system validates framework/database compatibility. Not all combinations are valid (e.g., JPA requires SQL databases, cannot target MongoDB or DynamoDB). See the Framework Compatibility Matrix below for details.
capacitor.config.yaml (comprehensive example):
version: "1.0"
# ── Project Metadata ──────────────────────────────────────
metadata:
name: "user-service-dal"
description: "Data access layer for the User domain"
organization: "Acme Corp"
license: "Apache-2.0"
website: "https://docs.acme.com/data/users"
contributors:
- "data-platform-team@acme.com"
# ── Source Configuration ──────────────────────────────────
sources:
models:
- "models/"
config:
- "config/"
# ── Namespace Configuration ───────────────────────────────
namespace: "com.acme.data.users"
# ── Dependencies (other Capacitor model packages) ─────────
dependencies:
com.acme.data.common:
version: "^1.2.0"
repository: "https://repo.acme.com/capacitor"
org.capacitor.stdlib.geo:
version: "~1.0.0"
# ── Plugin Ecosystem ──────────────────────────────────────
plugins:
repositories:
- "https://plugins.capacitor-lang.dev"
- "https://repo.acme.com/capacitor-plugins"
generators:
com.acme.capacitor:generator-django: "1.0.0"
decorators:
org.capacitor:decorator-lombok: "1.3.0"
org.capacitor:decorator-mapstruct: "1.1.0"
org.capacitor:decorator-opentelemetry: "1.2.0"
# ── Database Deployment Targets ───────────────────────────
targets:
development:
database: "postgres/15"
config:
host: "localhost"
port: 5432
database: "myapp_dev"
ssl: false
production:
database: "postgres/15"
provider: "aws/rds"
config:
host: "${env:DB_HOST}"
port: 5432
database: "myapp_prod"
ssl: true
encryption_at_rest: true
backup_retention: "30d"
ha: true
replicas: 2
analytics:
database: "dynamodb"
provider: "aws/dynamodb"
config:
region: "${env:AWS_REGION}"
billing_mode: "PAY_PER_REQUEST"
# ── Generator Configurations (SDK Generation) ─────────────
generators:
java-jpa-sdk:
generator: "capacitor:java-jpa"
target: "production"
output: "./generated/java"
service: "UserDataService" # Specifies which service to generate
options:
package: "com.acme.data.users"
jakartaEE: true
includeQueries: true
includeRepositories: true
includeDTOs: true
includeMappers: true
includeTests: true
includeFixtures: true
springBoot:
enabled: true
version: "3.2"
autoConfiguration: true
decorators:
- decorator: "org.capacitor:decorator-lombok"
config:
builder: true
data: true
slf4j: true
- decorator: "org.capacitor:decorator-mapstruct"
config:
componentModel: "spring"
- decorator: "org.capacitor:decorator-opentelemetry"
config:
traceRepositoryMethods: true
metricsEnabled: true
artifact:
groupId: "com.acme.data"
artifactId: "user-service-dal"
version: "2.1.0"
packaging: "jar"
publish:
repository: "https://repo.acme.com/maven"
snapshotRepository: "https://repo.acme.com/maven-snapshots"
typescript-prisma-sdk:
generator: "capacitor:typescript-prisma"
target: "production"
output: "./generated/typescript"
service: "UserDataService"
options:
includeQueries: true
includeTypes: true
strictNullChecks: true
esm: true
includeTests: true
artifact:
name: "@acme/user-service-dal"
version: "2.1.0"
packaging: "npm"
publish:
registry: "https://npm.acme.com"
python-sqlalchemy-sdk:
generator: "capacitor:python-sqlalchemy"
target: "production"
output: "./generated/python"
service: "UserDataService"
options:
package: "acme_data_users"
asyncSupport: true
typeHints: true
pydanticModels: true
includeQueries: true
includeTests: true
artifact:
name: "acme-user-service-dal"
version: "2.1.0"
packaging: "pypi"
publish:
repository: "https://pypi.acme.com"
# ── Projections (filtered views of the model) ─────────────
projections:
public-api:
description: "Public-facing types only, no internal entities"
transforms:
includeEntities:
- "User"
- "UserPreferences"
excludeEntities:
- "LoginHistory"
- "InternalAuditLog"
excludeTraits:
- "@internal"
includeShapes:
- "Address"
- "ContactInfo"
excludeFields:
User:
- "ssn"
- "internalNotes"
analytics-export:
description: "Read-only types for the analytics team"
transforms:
includeEntities:
- "User"
- "Session"
- "LoginHistory"
readOnly: true
excludeFields:
User:
- "ssn"
- "phone"
# ── Documentation Generation ──────────────────────────────
documentation:
html:
format: "html"
output: "./docs/api"
options:
title: "User Service Data Layer API"
includeERDiagram: true
includeAccessPatterns: true
includeCompatibilityMatrix: true
includeCodeExamples: true
exampleLanguages:
- "java"
- "typescript"
- "python"
theme: "default"
searchEnabled: true
markdown:
format: "markdown"
output: "./docs/md"
options:
singleFile: false
includeTableOfContents: true
# ── Linter Configuration ──────────────────────────────────
linter:
strict: false # Set true to treat warnings as errors
rules:
require-documentation:
level: warn
scope:
- "entities"
- "repositories"
require-access-patterns:
level: warn
message: "Entities should define at least one access pattern"
shimmed-features:
level: warn
message: "Feature requires application-level implementation"
unsupported-features:
level: error
message: "Feature not available for target"
naming-conventions:
level: warn
entities: "PascalCase"
fields: "camelCase"
enums: "SCREAMING_SNAKE_CASE"
max-entity-fields:
level: info
max: 30
performance-hints:
level: info
ignore:
- "deprecated-fields" # Ignore deprecation warnings during migration
# ── Compatibility Requirements ────────────────────────────
compatibility:
capacitorVersion: ">=1.0.0 <2.0.0"
generatorVersions:
capacitor:java-jpa: ">=1.2.0"
capacitor:typescript-prisma: ">=1.0.0"
Alternative: capacitor-build.json (for teams preferring JSON):
{
"version": "1.0",
"targets": {
"development": {
"target": "postgres/15",
"config": {
"host": "localhost",
"port": 5432,
"database": "myapp_dev"
}
},
"production": {
"target": "postgres/15",
"storage": "aws/rds",
"config": {
"host": "${env:DB_HOST}",
"port": 5432,
"database": "myapp_prod",
"ssl": true
}
}
},
"generators": {
"typescript-sdk": {
"language": "typescript",
"framework": "typeorm",
"output": "./src/generated",
"target": "development",
"options": {
"includeQueries": true,
"includeTypes": true,
"strictNullChecks": true
}
},
"java-sdk": {
"language": "java",
"framework": "jpa",
"output": "./src/main/java/com/example/dal",
"target": "production",
"options": {
"package": "com.example.dal",
"includeQueries": true,
"jakartaEE": true
}
}
}
}
Build commands:
# Generate all SDKs defined in config
capacitor build
# Generate specific SDK
capacitor build --generator typescript-sdk
# Generate all SDKs for a specific target
capacitor build --target development
# Override config with CLI flags
capacitor generate --lang typescript --framework prisma --output ./custom/path
Framework Compatibility Matrix
Not all framework/database combinations are valid. The Capacitor build system validates configurations and will error on incompatible combinations.
| Framework | PostgreSQL | MySQL | Oracle | SQL Server | MongoDB | DynamoDB | Cassandra | SQLite |
|---|---|---|---|---|---|---|---|---|
| Java/JPA | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| TypeScript/Prisma | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ |
| TypeScript/TypeORM | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| TypeScript/Sequelize | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Python/SQLAlchemy | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Go/GORM | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| C#/Entity Framework | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Swift/Core Data | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
Key Notes:
- SQL-focused frameworks (JPA, Entity Framework, SQLAlchemy, GORM, Sequelize) work with relational databases only
- TypeORM and Prisma have limited NoSQL support (MongoDB only)
- NoSQL databases (DynamoDB, Cassandra) require native SDKs rather than traditional ORMs
- Swift/Core Data primarily works with SQLite for local storage
Validation Errors:
When using incompatible combinations, Capacitor will error during build:
$ capacitor build --generator java-sdk
ERROR: Invalid configuration for generator 'java-sdk'
Framework 'jpa' is not compatible with target database 'dynamodb'
Compatible databases for JPA:
- PostgreSQL
- MySQL
- Oracle
- SQL Server
- SQLite
For DynamoDB, consider using:
- Native AWS SDK generation
- Language-specific DynamoDB clients
NoSQL SDK Generation:
For NoSQL databases, Capacitor generates native client code instead of ORM abstractions:
generators:
dynamodb-sdk:
language: "typescript"
framework: "aws-sdk" # Native AWS SDK
output: "./src/dal"
target: "production" # DynamoDB target
mongodb-sdk:
language: "typescript"
framework: "mongoose" # MongoDB native driver
output: "./src/dal"
target: "mongo-target"
Getting Started
1. Installation
The Capacitor CLI is distributed as self-contained native binaries for each platform. Releases are available on the Genairus GitHub releases page.
macOS
Homebrew (Recommended)
brew tap genairus/tap && brew install capacitor-cli
Manual (x86_64 and ARM)
mkdir -p capacitor-install/capacitor && \
curl -L https://github.com/genairus/capacitor/releases/latest/download/capacitor-cli-darwin-$(uname -m).zip \
-o capacitor-install/capacitor-cli.zip && \
unzip -qo capacitor-install/capacitor-cli.zip -d capacitor-install && \
mv capacitor-install/capacitor-cli-darwin-*/* capacitor-install/capacitor
sudo capacitor-install/capacitor/install
Linux
Manual (x86_64 and ARM)
mkdir -p capacitor-install/capacitor && \
curl -L https://github.com/genairus/capacitor/releases/latest/download/capacitor-cli-linux-$(uname -m).zip \
-o capacitor-install/capacitor-cli.zip && \
unzip -qo capacitor-install/capacitor-cli.zip -d capacitor-install && \
mv capacitor-install/capacitor-cli-linux-*/* capacitor-install/capacitor
sudo capacitor-install/capacitor/install
Windows
Scoop (Recommended)
scoop bucket add genairus https://github.com/genairus/scoop-bucket; `
scoop install genairus/capacitor-cli
Manual (x64)
Download capacitor-cli-windows-x86_64.zip from the releases page, extract it, then run install.bat as Administrator.
Verify Installation
capacitor --version
2. Initialize Project
mkdir my-data-project && cd my-data-project
capacitor init
# Output:
# ✓ Created capacitor.config.yaml
# ✓ Created models/ directory
# ✓ Created weaves/ directory
3. Define Your First Model
Create models/user.capacitor:
enum UserStatus {
ACTIVE,
INACTIVE,
SUSPENDED
}
shape ContactInfo {
@pii
email: Email,
@pii
phone: Phone?
}
entity User {
@identity
@default(value: "uuid()")
userId: UUID,
@unique
@access(type: "global", name: "ByUsername")
username: String,
@pii
@access(type: "global", name: "ByEmail")
contact: ContactInfo,
status: UserStatus,
@default(value: "now()")
createdAt: Timestamp,
lastLoginAt: Timestamp?
}
4. Configure Build Targets
Create capacitor.config.yaml:
version: "1.0"
targets:
development:
target: "postgres/15"
config:
host: "localhost"
port: 5432
database: "myapp_dev"
ssl: false
generators:
typescript-sdk:
language: "typescript"
framework: "typeorm"
output: "./sdk"
target: "development"
options:
includeQueries: true
includeTypes: true
Or create weaves/development.capacitor for database-only config:
weave Development {
target: "postgres/15",
entities: [User],
config {
host: "localhost",
port: 5432,
database: "myapp_dev",
ssl: false
}
}
5. Check and Plan
# Validate model
capacitor check
# Preview changes
capacitor plan --target development
6. Apply Changes
capacitor apply --target development
7. Generate SDK
# Using build config (recommended)
capacitor build
# Or with CLI flags
capacitor generate --lang typescript --framework typeorm --output ./sdk
8. Use in Your Application
import { CapacitorClient } from './sdk';
const db = new CapacitorClient({
host: 'localhost',
port: 5432,
database: 'myapp_dev'
});
// Create user
const user = await db.users.create({
username: "john_doe",
contact: {
email: "john@example.com",
phone: "+1-555-0123"
},
status: UserStatus.ACTIVE
});
// Query by username
const found = await db.users.findByUsername("john_doe");
// Query by email
const byEmail = await db.users.findByEmail("john@example.com");
Generator Architecture
Capacitor uses a generator architecture that separates the universal data model from database-specific implementations.
How Generators Work
- Parse Capacitor models into an abstract syntax tree (AST)
- Validate semantic rules (e.g., entities must have identity)
- Check target compatibility and emit warnings for shimmed features
- Generate target-specific code:
- DDL for databases (SQL, MongoDB schemas, DynamoDB tables)
- Type-safe SDK code (TypeScript, Go, Python, etc.)
- Migration scripts
- Application shims where needed
Support Matrix
Feature support is documented in the Generator Implementation Guide, which provides detailed information on:
- Native vs. shimmed implementations per database
- Performance characteristics and trade-offs
- Code examples for each generator
- Migration patterns between databases
Creating Custom Generators
Capacitor's generator architecture is pluggable. You can create custom generators for:
- Custom ORMs (e.g., Django ORM, SQLAlchemy)
- Proprietary databases
- GraphQL schemas
- OpenAPI specifications
- Infrastructure as Code (Terraform, CloudFormation)
Integration with Other Languages
Capacitor is part of the Genairus Software Factory ecosystem. It integrates seamlessly with all other generative domain languages:
With Chronos (Requirements)
Chronos journeys can reference Capacitor entities:
namespace com.acme.requirements.checkout
use com.acme.data.orders#Order
use com.acme.data.orders#OrderStatus
journey PlaceOrder {
steps: [
step CreateOrder {
expectation: "Order entity created with status PENDING"
}
]
}
Benefit: Requirements directly reference your data model for end-to-end traceability.
Optional trait: Document which journeys create/modify entities:
namespace com.acme.data.orders
use com.acme.requirements.checkout#PlaceOrder
@journey("PlaceOrder")
entity Order {
@required @id
id: String
status: OrderStatus
}
With Smithy (API Contracts)
Smithy operations use Capacitor entities for request/response shapes:
namespace com.acme.api.orders
use com.acme.data.orders#Order
operation GetOrder {
input: GetOrderInput
output: GetOrderOutput
}
structure GetOrderOutput {
@required
order: Order // Capacitor entity
}
Benefit: API contracts and data models stay perfectly aligned. Change the entity, and API types update automatically.
With Flux (User Interface)
Flux components use generated SDKs from Capacitor:
namespace com.acme.ui.orders
use com.acme.data.orders#Order
component OrderList {
props: {
orders: List<Order> // Type from Capacitor
}
layout: column {
for order in orders {
OrderCard(order: order)
}
}
}
Benefit: UI components are type-safe against your data model. Generation-time errors if you reference a non-existent field.
With Fusion (Infrastructure)
Fusion deploys Capacitor schemas as databases:
namespace com.acme.infra
service OrderService {
database: {
provider: postgres,
schema: "../data/orders.capacitor",
migrations: auto
}
}
Benefit: Database infrastructure defined once, deployed to any cloud.
Standalone Mode
Capacitor works perfectly without other languages:
namespace com.acme.data.orders
entity Order {
@required @id
id: String
items: List<OrderItem>
total: Money
}
Generate schemas, migrations, and SDKs without any other dependencies.
Configuration for Integration
Enable integrations in capacitor-build.json:
{
"version": "1.0",
"sources": ["./data/**/*.capacitor"],
"generators": {
"postgres-sdk": {
"generator": "capacitor:java-jpa",
"target": "production-sql"
}
},
"integrations": {
"chronos": {
"requirements": "./requirements"
},
"smithy": {
"spec": "./api"
},
"flux": {
"components": "./ui"
},
"fusion": {
"infra": "./infrastructure"
}
}
}
When integrations are enabled, the validator validates cross-language references and provides type checking.
Example: Complete E-Commerce Stack
Capacitor defines data:
entity Order {
@required @id
id: String
total: Money
status: OrderStatus
}
Chronos references it:
use com.acme.data.orders#Order
journey PlaceOrder {
steps: [
step CreateOrder {
expectation: "Order entity created"
}
]
}
Smithy uses it:
use com.acme.data.orders#Order
operation GetOrder {
output: GetOrderOutput
}
structure GetOrderOutput {
order: Order
}
Flux displays it:
use com.acme.data.orders#Order
component OrderSummary {
props: {
order: Order
}
}
Fusion deploys it:
service OrderService {
database: {
schema: "../data/orders.capacitor"
}
}
For complete integration guide, see Languages Overview.
Appendix
Supported Targets
Databases
- PostgreSQL 12+
- MySQL 8.0+
- Oracle 19c+
- MS SQL Server 2019+
- MongoDB 5.0+
- DynamoDB
- Cassandra 4.0+
- SQLite 3.35+
- CockroachDB 22.0+
ORMs and Frameworks
- JPA (Hibernate 5.6+, Jakarta EE 9+)
- Prisma 4.0+
- TypeORM 0.3+
- Sequelize 6.0+
- GORM (Go)
- SQLAlchemy (Python)
Supported SDK Languages
- TypeScript/JavaScript
- Go
- Python
- Java
- Scala
- Rust
- C#
- Swift
Further Reading
- Generator Implementation Guide - Detailed implementation for each database