ADR-024: Application Dictionary Metadata Caching

<!-- MADR 3.0 Template - Markdown Any Decision Records --> <!-- Reference: https://adr.github.io/madr/ -->

Status

Proposed

Date

2025-12-07

Deciders

Context and Problem Statement

The RegistryToolLogic currently executes SQL queries against the live iDempiere database for every AD metadata request (describeTable, listTables, listWindows, etc.). This has several issues:

  1. Repeated Queries: Same static data queried repeatedly (AD metadata rarely changes)
  2. Latency: 3+ SQL queries per describeTable call (~50-100ms each)
  3. Database Load: Unnecessary load on production database
  4. Offline Unavailable: Cannot work without database connection

Application Dictionary metadata is static - it only changes during development or plugin installation. Caching this data provides significant performance and availability benefits.

Decision Drivers

Considered Options

  1. Quarkus Cache (@CacheResult) - In-memory caching with annotations
  2. File-based JSON Cache - Export AD metadata to JSON files
  3. RAG as Primary Store - Use vector embeddings for all lookups
  4. Hybrid: Quarkus Cache + File Persistence - Memory cache backed by file
  5. Redis/External Cache - Distributed cache server

Decision Outcome

Chosen option: "Hybrid: Quarkus Cache + File Persistence", because:

Confirmation

The decision is confirmed when:

Pros and Cons of the Options

Option 1: Quarkus Cache (@CacheResult)

Simple annotation-based in-memory caching.

@CacheResult(cacheName = &quot;ad-tables&quot;)
public ToolResult describeTable(String tableName, String language) {
    // SQL query - only executed on cache miss
}

Option 2: File-based JSON Cache

Export AD metadata to JSON files on disk.

~/.idempiere-cli/cache/
├── ad-tables.json       # All table metadata
├── ad-windows.json      # All window metadata
├── ad-processes.json    # All process metadata
└── cache-meta.json      # Checksums, timestamps

Option 3: RAG as Primary Store

Use existing vector store for all lookups, not just semantic search.

Option 4: Hybrid Quarkus Cache + File Persistence (chosen)

Combine in-memory cache with file-based persistence.

@ApplicationScoped
public class ADMetadataCache {

    @Inject
    @CacheName(&quot;ad-metadata&quot;)
    Cache cache;

    @ConfigProperty(name = &quot;idempiere.cache.dir&quot;)
    String cacheDir;  // ~/.idempiere-cli/cache/

    @PostConstruct
    void loadFromFile() {
        // Load persisted cache on startup
    }

    @PreDestroy
    void persistToFile() {
        // Save cache to file on shutdown
    }

    @CacheResult(cacheName = &quot;ad-tables&quot;)
    public TableMetadata getTable(String tableName) {
        // Fetch from DB on miss, cached thereafter
    }
}

Option 5: Redis/External Cache

Distributed cache server.

Architecture

Cache Layers

┌─────────────────────────────────────────────────────────────────────┐
│                         Query Request                                │
│                    describeTable(&quot;C_Order&quot;)                          │
└─────────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────────┐
                    │   L1: Memory Cache  │  ← Quarkus @CacheResult
                    │   (Caffeine)        │     ~0.1ms lookup
                    └─────────────────────┘
                              │ miss
                              ▼
                    ┌─────────────────────┐
                    │   L2: File Cache    │  ← JSON persistence
                    │   (~/.idempiere-cli)│     ~5ms lookup
                    └─────────────────────┘
                              │ miss
                              ▼
                    ┌─────────────────────┐
                    │   L3: Database      │  ← Live SQL query
                    │   (PostgreSQL)      │     ~50-100ms
                    └─────────────────────┘

Cache Key Strategy

Cache Key Format: {entity_type}:{identifier}:{language}

Examples:
- table:C_Order:en_US
- table:C_Order:de_DE
- window:Sales Order:en_US
- process:DocumentProcess:en_US

Cache Invalidation

Event Invalidation Strategy
CLI startup Load from file cache
registry cache refresh Clear all, rebuild from DB
Plugin install detected Invalidate affected entries
AD_Table sync Invalidate specific table
TTL expiry (24h default) Lazy refresh on next access

Data Structures

/**
 * Cached table metadata - immutable after creation.
 */
public record CachedTable(
    int id,
    String tableName,
    String name,
    String description,
    String help,
    boolean isView,
    String accessLevel,
    String entityType,
    List&lt;CachedColumn&gt; columns,
    List&lt;CachedWindowTab&gt; windowsAndTabs,
    Instant cachedAt
) {}

/**
 * Cache metadata for invalidation.
 */
public record CacheMetadata(
    Instant lastRefresh,
    String adChecksum,  // Hash of AD_Table count + max Updated
    int tableCount,
    int windowCount,
    int processCount
) {}

Implementation Plan

Phase 1: Quarkus Cache Integration

  1. Add quarkus-cache dependency
  2. Annotate RegistryToolLogic methods with @CacheResult
  3. Configure cache size and TTL in application.properties
  4. Add cache statistics to getStatistics() result

Phase 2: File Persistence

  1. Create ADMetadataCache service
  2. Implement JSON serialization for cache entries
  3. Load cache on startup, persist on shutdown
  4. Add registry cache CLI commands

Phase 3: Smart Invalidation

  1. Compute AD checksum (table count + max updated timestamp)
  2. Compare checksums on startup to detect changes
  3. Selective invalidation for specific tables
  4. Background refresh for stale entries

CLI Commands

# Show cache status
idempiere-cli registry cache status

# Force refresh from database
idempiere-cli registry cache refresh

# Clear cache
idempiere-cli registry cache clear

# Export cache to file (for offline use)
idempiere-cli registry cache export --output ad-cache.json

# Import cache from file
idempiere-cli registry cache import --input ad-cache.json

Configuration

# application.properties

# Cache configuration
idempiere.cache.enabled=true
idempiere.cache.dir=${user.home}/.idempiere-cli/cache
idempiere.cache.ttl=24h
idempiere.cache.max-entries=5000

# Quarkus Cache (Caffeine)
quarkus.cache.caffeine.ad-tables.maximum-size=1000
quarkus.cache.caffeine.ad-tables.expire-after-access=24h
quarkus.cache.caffeine.ad-windows.maximum-size=500
quarkus.cache.caffeine.ad-processes.maximum-size=500

Integration with RAG

The caching layer complements RAG (ADR-021):

Use Case Solution
Exact lookup: describeTable(&quot;C_Order&quot;) Cache (fast, precise)
Pattern search: listTables(&quot;%Order%&quot;) SQL (LIKE query)
Semantic search: "tables for sales" RAG (vector similarity)
Smart search: auto-detect Cache → SQL → RAG fallback
┌─────────────────────────────────────────────────────────────────────┐
│                        smartSearch(query)                            │
└─────────────────────────────┬───────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐     ┌───────────────┐     ┌───────────────┐
│ Exact Name    │     │ SQL Pattern   │     │ Natural Lang  │
│ &quot;C_Order&quot;     │     │ &quot;%Order%&quot;     │     │ &quot;sales tables&quot;│
└───────────────┘     └───────────────┘     └───────────────┘
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐     ┌───────────────┐     ┌───────────────┐
│    CACHE      │     │     SQL       │     │     RAG       │
│   (fastest)   │     │   (precise)   │     │  (semantic)   │
└───────────────┘     └───────────────┘     └───────────────┘

Metrics and Monitoring

// Cache statistics exposed via getStatistics()
{
  &quot;cache&quot;: {
    &quot;enabled&quot;: true,
    &quot;hitRate&quot;: 0.85,
    &quot;missRate&quot;: 0.15,
    &quot;size&quot;: 150,
    &quot;maxSize&quot;: 1000,
    &quot;lastRefresh&quot;: &quot;2025-12-07T10:00:00Z&quot;,
    &quot;fileCache&quot;: {
      &quot;exists&quot;: true,
      &quot;size&quot;: &quot;2.5MB&quot;,
      &quot;entries&quot;: 850
    }
  }
}

References

Path: /docs/developers/architecture/idempiere-hub/024-ad-metadata-caching