Organizing Metadata for Large Projects
When building enterprise-scale applications with ObjectQL, proper metadata organization becomes critical. This guide demonstrates best practices for structuring your object definitions, actions, hooks, and translations in a maintainable way.
The Challenge
As your application grows beyond 30-50 objects, a flat file structure becomes problematic:
Problems with Flat Structure:
src/objects/
├── user.object.yml
├── organization.object.yml
├── account.object.yml
├── contact.object.yml
├── opportunity.object.yml
├── employee.object.yml
├── department.object.yml
├── invoice.object.yml
├── payment.object.yml
├── project.object.yml
├── task.object.yml
... (100+ files)- ❌ Hard to find related objects
- ❌ Merge conflicts when multiple teams work simultaneously
- ❌ Unclear ownership boundaries
- ❌ Can't deploy modules independently
- ❌ Difficult to understand relationships
Recommended Structure: Domain-Driven Modules
For applications with 30+ objects and multiple teams, organize by business domain:
src/
├── core/ # Foundation objects (user, org, etc.)
│ ├── objects/
│ ├── i18n/
│ └── index.ts
│
├── modules/ # Business domain modules
│ ├── crm/ # Customer management
│ │ ├── objects/
│ │ ├── actions/
│ │ ├── hooks/
│ │ ├── i18n/
│ │ ├── README.md
│ │ └── index.ts
│ │
│ ├── hr/ # Human resources
│ ├── finance/ # Finance & accounting
│ └── project/ # Project management
│
├── extensions/ # Custom overrides
├── shared/ # Shared utilities
└── index.ts # Application entryReal-World Example
See the complete working example at:
examples/scenarios/enterprise-structure/This demonstrates:
- ✅ 20+ objects organized across 5 modules
- ✅ Domain-driven structure (CRM, HR, Finance, Project)
- ✅ Cross-module relationships
- ✅ Extension pattern for customization
- ✅ Comprehensive indexing strategy
- ✅ Multi-language support (en, zh-CN)
- ✅ Module documentation
Module Anatomy
Each module follows a consistent pattern:
modules/[domain]/
├── objects/ # Object definitions
│ ├── [object1].object.yml
│ └── [object2].object.yml
├── actions/ # Custom actions
│ └── [object1].action.ts
├── hooks/ # Lifecycle hooks
│ └── [object1].hooks.ts
├── i18n/ # Translations
│ ├── en/
│ └── zh-CN/
├── README.md # Module documentation
└── index.ts # Module exportsModule Documentation Template
Each module should have a README explaining:
- Overview - What business domain it covers
- Objects - List of objects with descriptions
- Relationships - How objects relate to each other
- Team Ownership - Who maintains this module
- Dependencies - What other modules/objects it depends on
- Usage Examples - Common query patterns
Object Naming Conventions
Prefixing Strategy
For large projects with multiple modules, use prefixes to avoid name collisions:
# ✅ Good: Clear module ownership
name: crm_account
name: finance_invoice
name: project_task
# ❌ Avoid: Risk of collision
name: account # Which account? CRM or Finance?
name: task # Project task or general task?When to prefix:
- ✅ Multi-module applications (30+ objects)
- ✅ Plugin architectures
- ✅ When similar concepts exist across domains
When NOT to prefix:
- ❌ Core shared objects (
user,organization) - ❌ Small applications (< 30 objects)
- ❌ When it reduces clarity
Dependency Management
Dependency Layers
┌─────────────────────────────────┐
│ Application Layer │
│ (modules/*) │
│ - Can depend on Core │
│ - Can depend on other modules │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Foundation Layer │
│ (core/*) │
│ - No dependencies │
│ - Used by everyone │
└─────────────────────────────────┘Cross-Module References
When modules need to reference each other's objects:
# In modules/finance/objects/invoice.object.yml
fields:
account:
type: lookup
reference_to: crm_account # Reference to CRM moduleBest Practices:
- Document cross-module dependencies in module README
- Avoid circular dependencies
- Use core objects to break dependency cycles
- Consider extracting shared concepts to core layer
Index Strategy by Module
Different modules have different performance requirements:
High-Traffic Modules (CRM, Sales)
# Aggressive indexing
fields:
status: { type: select, index: true }
owner: { type: lookup, index: true }
created_at: { type: datetime, index: true }
indexes:
owner_status_idx: { fields: [owner, status] }
status_created_idx: { fields: [status, created_at] }Low-Traffic Modules (Admin, Config)
# Minimal indexing
fields:
name: { type: text, index: true }
status: { type: select, index: true }
# Add more indexes only when neededExtension Pattern
Use extensions to customize objects without modifying source:
Original (core/objects/user.object.yml):
name: user
fields:
name: { type: text }
email: { type: text }Extension (extensions/user.extension.object.yml):
name: user # Same name triggers merge
fields:
employee_id: { type: text }
email: { required: true, unique: true }Result: ObjectQL merges both definitions, adding employee_id and making email required.
Internationalization at Scale
Three-Layer i18n Strategy
1. Core Layer (core/i18n/)
→ Shared objects (user, organization)
2. Module Layer (modules/[domain]/i18n/)
→ Domain-specific objects
3. Extension Layer (extensions/i18n/)
→ Customer/regional customizationsDirectory Structure
core/i18n/
en/core.json
zh-CN/core.json
modules/crm/i18n/
en/crm.json
zh-CN/crm.jsonMigration Path
From Flat to Modular
Step 1: Analyze Group existing objects by business domain.
Step 2: Plan Create module structure, decide on prefixes.
Step 3: Migrate Gradually
src/
├── objects/ # Legacy (keep temporarily)
├── modules/ # New structure
│ └── crm/ # Start with one module
└── index.ts # Loads from bothStep 4: Update References Update imports and references to use new module structure.
Step 5: Clean Up Remove legacy flat structure once migration is complete.
Project Size Guidelines
| Size | Objects | Teams | Recommended Structure |
|---|---|---|---|
| Small | 1-30 | 1 | Flat objects/ directory |
| Medium | 30-100 | 2-3 | Domain modules |
| Large | 100-500 | 5-10 | Modules + plugins |
| Enterprise | 500+ | 10+ | Monorepo with packages |
Testing Strategy
Module Tests
// modules/crm/__tests__/integration.test.ts
describe('CRM Module', () => {
it('should handle lead conversion', async () => {
const lead = await createLead();
await convertLead(lead.id);
// Verify account, contact, opportunity created
});
});Object Schema Tests
// modules/crm/objects/__tests__/account.test.ts
describe('Account Object', () => {
it('should have required fields', () => {
const schema = loadObjectSchema('crm_account');
expect(schema.fields.name.required).toBe(true);
});
});Complete Working Example
Explore the full example with 20+ objects:
cd examples/scenarios/enterprise-structure
pnpm install
pnpm buildThe example includes:
- Core module (user, organization, attachment)
- CRM module (account, contact, opportunity, lead)
- HR module (employee, department, position, timesheet)
- Finance module (invoice, payment, expense, budget)
- Project module (project, task, milestone, timesheet entry)
- Extensions pattern
- Multi-language support
- Comprehensive documentation
Key Takeaways
- Start simple - Don't over-engineer for small projects
- Think in domains - Organize by business capability, not technical layers
- Document boundaries - Make module ownership and dependencies explicit
- Plan for scale - Use prefixes and modules when you hit 30-50 objects
- Test modules - Each module should be testable independently
- Version control - Use module-level versioning for independent deployments