From Metadata to Full Application: How qqq Works
Dive deep into the qqq architecture and understand how simple metadata definitions become fully functional enterprise applications.

When you first see qqq code, it looks almost too simple. Define a table with a few fields, and suddenly you have CRUD operations, REST APIs, a complete admin UI, and audit logging. How does that work?
The core insight behind qqq is that most application logic can be expressed as metadata. Tables, fields, relationships, permissions, and workflows - they're all just data describing your application.
Understanding the architecture helps you work with qqq more effectively and know where to add custom behavior when needed.
The Metadata Layer
Everything in qqq starts with metadata. The QInstance object is the root - it holds all the metadata that defines your application:
QInstance qInstance = new QInstance();
qInstance.addTable(new OrderTableMetaData());
qInstance.addTable(new CustomerTableMetaData());
qInstance.addProcess(new ApproveOrderProcess());
qInstance.addApp(new AdminAppMetaData());Core metadata types include:
- Tables: Define your data structures with fields, types, and validation rules
- Processes: Multi-step workflows that can include human approvals
- Reports: Aggregate queries with configurable parameters
- Apps: Group tables and processes into logical applications
- Permissions: Role-based access at table and field level
Metadata is typically defined in MetaDataProducer classes that implement patterns for consistency across your codebase.
The Runtime Engine
When qqq starts, it reads your metadata and constructs the runtime:
- Backend Initialization: Each table is connected to its backend (database, API, etc.)
- Action Registration: CRUD actions are created for each table
- API Generation: REST endpoints are registered with OpenAPI documentation
- UI Generation: The admin UI is configured to render your tables and processes
The key insight: the runtime doesn't generate code files. It creates live objects that handle requests. This means changes to metadata take effect immediately in development mode.
How a Request Flows
When a user queries the orders table, here's what happens:
- Request Received: Javalin routes the REST call to the QueryAction
- Permission Check: The framework verifies the user has read access to the table
- Filter Processing: Query filters are validated and converted to backend-specific syntax
- Backend Execution: The query runs against the configured backend (PostgreSQL, MongoDB, etc.)
- Post-Processing: Custom post-query hooks run if configured
- Response Formatting: Results are serialized and returned
Every step is extensible. You can inject custom behavior at any point without rewriting the core flow.
Extensibility Points
qqq provides several patterns for customization:
Pre/Post Hooks: Run custom code before or after any action:
new PreInsertCustomizer()
.withTableName("order")
.withFunction((record) -> {
record.setValue("createdAt", Instant.now());
return record;
});Custom Actions: Replace standard behavior entirely when needed:
new CustomTableAction()
.withTableName("order")
.withActionType(ActionType.INSERT)
.withActionClass(MyCustomInsertAction.class);Process Steps: Build complex workflows from composable steps:
new QProcessMetaData()
.withName("fulfillOrder")
.withSteps(List.of(
new ValidateInventoryStep(),
new ReserveInventoryStep(),
new CreateShipmentStep()
));The UI Layer
The admin UI is a React application that reads table metadata and renders appropriate components:
- Field types determine input components (date picker, dropdown, text field)
- Relationships render as linked searches
- Permissions control what's visible and editable
- Custom widgets can be registered for complex fields
You can customize the UI progressively: change field labels, hide columns, or replace entire components while keeping the rest generated.
Why This Architecture Matters
Traditional frameworks require you to write each layer: entity, repository, service, controller, DTO, tests. qqq generates these from metadata, but crucially, it doesn't hide them. You can see exactly what's happening and override any piece.
The result: you write 10x less code for standard operations, and when you need custom behavior, you have full access to the underlying Java.
Learn More
Ready to understand qqq in depth? Check out our architecture documentation or start with the quickstart tutorial to see these concepts in action.