diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md
new file mode 100644
index 0000000..13cd192
--- /dev/null
+++ b/cds-feature-ai-core/docs/architecture.md
@@ -0,0 +1,202 @@
+# Architecture: `cds-feature-ai-core`
+
+## Table of Contents
+
+- [Purpose](#purpose)
+- [Dependencies](#dependencies)
+- [Feature](#feature)
+ - [CDS Model](#cds-model)
+ - [Public API](#public-api)
+ - [Key Infrastructure Classes](#key-infrastructure-classes)
+ - [Multi-Tenancy](#multi-tenancy)
+ - [Key Flows](#key-flows)
+ - [Tenant Subscribe](#tenant-subscribe)
+ - [Tenant Unsubscribe](#tenant-unsubscribe)
+ - [Inference Client Resolution](#inference-client-resolution)
+- [Tests](#tests)
+- [Quality Tools](#quality-tools)
+
+---
+
+## Purpose
+
+Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a standard CAP `RemoteService`. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap.
+
+→ [README](../README.md)
+
+---
+
+## Dependencies
+
+| Dependency | Why |
+|---|---|
+| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. |
+| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). |
+| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. |
+| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. |
+
+---
+
+## Feature
+
+### CDS Model
+
+Defined in `src/main/resources/cds/`: `AICore.cds`
+
+```cds
+// @protocol: 'none' — programmatic access only, never exposed as OData/REST
+service AICore {
+ @cds.persistence.skip entity resourceGroups { ... } // AI Core resource group
+ @cds.persistence.skip entity deployments { ... } // AI Core deployment + action stop()
+ @cds.persistence.skip entity configurations { ... } // AI Core configuration
+
+ // Events:
+ event resourceGroup // in: (optional: tenantId — falls back to UserInfo.getTenant() from request context) → out: resourceGroupId
+ event deploymentId // in: resourceGroupId + ModelDeploymentSpec → out: deploymentId
+ event inferenceClient // in: resourceGroupId + deploymentId → out: ApiClient
+}
+```
+
+Entities are `@cds.persistence.skip` — they have no database tables and are backed entirely by the AI Core REST API at runtime.
+
+---
+
+### Public API
+
+→ [Programmatic Usage in README](../README.md#programmatic-usage)
+
+---
+
+### Key Infrastructure Classes
+
+| Class | Role |
+|---|---|
+| `AICoreServiceConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers, clients, and caches at startup; detects AI Core binding |
+| `AICoreConfig` | Immutable config record populated from `cds.ai.core.*` YAML properties |
+| `AICoreClients` | Holds `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and the raw `AiCoreService` from the AI SDK |
+| `DeploymentResolver` | Thread-safe resolver with two Caffeine caches (`tenantId → rgId`, `rgId::configName → deploymentId`) and `ConcurrentHashMap` per-key locks (prevents duplicate deployments under concurrency); Resilience4j backoff on 403/404/412 |
+| `AICoreApiHandler` | `@On` handler for the three custom events: `resourceGroup`, `deploymentId`, `inferenceClient` |
+| `AICoreSetupHandler` | `@After(LATE) SubscribeEvent` / `@Before(EARLY) UnsubscribeEvent` — creates/deletes resource groups during MTX tenant lifecycle |
+| `AbstractCrudHandler` | Base for all entity CRUD handlers; provides `resolveResourceGroup()` and `ensureResourceGroupAccessible()` (tenant isolation guard) |
+
+---
+
+### Multi-Tenancy
+
+→ [Multi-Tenancy in README](../README.md#multi-tenancy)
+
+---
+
+### Key Flows
+
+#### Tenant Subscribe
+
+```
+CAP MTX DeploymentService
+ |
+ | SubscribeEvent @After(LATE)
+ v
+AICoreSetupHandler
+ |
+ | resolveResourceGroup(tenantId)
+ v
+DeploymentResolver
+ |
+ | GET /v2/admin/resourceGroups?labelFilter=CDS_TENANT_ID=tenantId
+ v
+SAP AI Core
+ |
+ | (if absent) POST /v2/admin/resourceGroups
+ v
+resourceGroupId (cached 1h after last access — subsequent calls skip the AI Core management API;
+ if a resource group is deleted or reassigned externally, the plugin won't notice until the cache expires after 1h or the app restarts)
+```
+
+#### Inference Client Resolution
+
+##### Event 1: resourceGroup
+
+```mermaid
+flowchart TD
+ A1["emit ResourceGroupContext (tenantId)"]
+ A1 --> A2{"multiTenancy enabled
AND tenantId != null?"}
+ A2 -->|no| A3["return config.defaultResourceGroup()"]
+ A2 -->|yes| A4{"tenantResourceGroupCache
lookup by tenantId"}
+ A4 -->|cache hit| A5["return cached resourceGroupId"]
+ A4 -->|cache miss| A6["GET /v2/admin/resourceGroups
labelSelector: ext.ai.sap.com/tenant={tenantId}"]
+ A6 --> A7{"found?"}
+ A7 -->|yes| A8["cache result (expireAfterAccess 1h)"]
+ A7 -->|no| A9["POST /v2/admin/resourceGroups
(handle 409 Conflict = already exists)"]
+ A9 --> A8
+ A8 --> A5
+```
+
+##### Event 2: deploymentId — invoked with `resourceGroupId`
+
+```mermaid
+flowchart TD
+ B1["emit DeploymentIdContext
(resourceGroupId, ModelDeploymentSpec)"]
+ B1 --> B2["acquire per-key lock
(ConcurrentHashMap)"]
+ B2 --> B3{"deploymentCache
lookup by rgId::configName"}
+ B3 -->|cache hit| B4["validateCachedDeployment:
GET /v2/lm/deployments/{id}"]
+ B4 --> B5{"status RUNNING or PENDING?"}
+ B5 -->|yes| B6["return cached deploymentId"]
+ B5 -->|no / 404| B7["invalidate cache entry"]
+ B7 --> B8
+ B3 -->|cache miss| B8["findOrCreateDeployment (under lock)"]
+ B8 --> B9["queryDeploymentsUntilReady (with retry):
GET /v2/lm/deployments?scenarioId=..."]
+ B9 --> B10{"match by configName
+ matchesExisting() + RUNNING/PENDING?"}
+ B10 -->|found| B11["cache deploymentId (expireAfterAccess 1h)"]
+ B10 -->|not found| B12["findOrCreateConfiguration:
GET /v2/lm/configurations?scenarioId=..."]
+ B12 --> B13{"config with matching name exists?"}
+ B13 -->|yes| B14["reuse existing configId"]
+ B13 -->|no| B15["POST /v2/lm/configurations"]
+ B15 --> B14
+ B14 --> B16["POST /v2/lm/deployments (with retry for 403/412)"]
+ B16 --> B17["pollUntilRunning:
GET /v2/lm/deployments/{id}
(exponential backoff)"]
+ B17 --> B11
+ B11 --> B6
+```
+
+*Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).*
+
+##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId`
+
+```mermaid
+flowchart TD
+ C1["emit InferenceClientContext
(resourceGroupId, deploymentId)"]
+ C1 --> C2["clients.sdkService()
.getInferenceDestination(rgId)
.usingDeploymentId(depId)"]
+ C2 --> C3["return ApiClient.create(destination)"]
+```
+
+
+#### Tenant Unsubscribe
+
+```
+CAP MTX DeploymentService
+ |
+ | UnsubscribeEvent @Before(EARLY)
+ v
+AICoreSetupHandler
+ |
+ | DELETE /v2/admin/resourceGroups/{id}
+ v
+SAP AI Core
+ |
+ | invalidateTenant(tenantId) — evicts tenantResourceGroupCache and deploymentCache (which was filled on first call to resolveDeployment) entries for this tenant
+ v
+(done)
+```
+---
+
+## Tests
+
+Unit tests for `AICoreServiceConfiguration`, `AICoreServiceImpl`, `AICoreSetupHandler`, and the CRUD handlers live in `src/test/` within this module (`mvn test`).
+
+End-to-end integration tests against a real AI Core instance live in [`integration-tests/spring/`](../../integration-tests/README.md) (`AICoreServiceTest`, `DeploymentTest`, `ResourceGroupTest`, `MultiTenancyTest`, and others). MTX lifecycle tests (subscribe/unsubscribe/tenant isolation) live in [`integration-tests/mtx-local/`](../../integration-tests/README.md).
+
+---
+
+## Quality Tools
+
+→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks)
diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md
new file mode 100644
index 0000000..b740d57
--- /dev/null
+++ b/cds-feature-recommendations/docs/architecture.md
@@ -0,0 +1,132 @@
+# Architecture: `cds-feature-recommendations`
+
+## Table of Contents
+
+- [Purpose](#purpose)
+- [Dependencies](#dependencies)
+- [Feature](#feature)
+ - [CDS Model](#cds-model)
+ - [Configuration](#configuration)
+ - [Public API / Handlers](#public-api--handlers)
+ - [Key Infrastructure Classes](#key-infrastructure-classes)
+ - [Multi-Tenancy](#multi-tenancy)
+ - [Key Flows](#key-flows)
+ - [Recommendation Pipeline (OData GET on draft entity)](#recommendation-pipeline-odata-get-on-draft-entity)
+ - [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation)
+- [Tests](#tests)
+- [Quality Tools](#quality-tools)
+
+---
+
+## Purpose
+
+Automatically injects AI-powered field recommendations from the SAP RPT-1 tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required.
+
+→ [README](../README.md)
+
+---
+
+## Dependencies
+
+| Dependency | Why |
+|---|---|
+| [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. |
+| `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. |
+| `com.sap.ai.sdk.foundationmodels:sap-rpt` (SAP AI SDK) | Provides the RPT-1 model client used to call the `/predict` endpoint. |
+| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). |
+| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. |
+
+---
+
+## Feature
+
+### CDS Model
+
+No dedicated CDS model file — the plugin relies on the `AICore` service model provided by `cds-feature-ai-core`, and on the `SAP_Recommendations` navigation property injected by the `@cap-js/ai` Node.js plugin (or added manually by the application).
+
+The Node plugin will automatically detect fields annotated with a value list, see [`README`](../README.md#enabling-recommendations).
+
+### Configuration
+
+Wired by `RecommendationConfiguration` (extends `CdsRuntimeConfiguration`) at startup. It detects whether an AI Core binding is present and selects production vs. mock mode accordingly — no manual activation is required.
+
+### Public API / Handlers
+
+No Java API — the plugin is entirely annotation-driven. Extend or annotate your CDS model to control which fields receive recommendations, see [`README`](../README.md#enabling-recommendations).
+Currently, it is not possible to hook into the recommendation result from application code to observe the injected output, nor to override the inference call itself ([#110](https://github.com/cap-java/cds-ai/issues/110)).
+
+### Key Infrastructure Classes
+
+| Class | Role |
+|---|---|
+| `RecommendationConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers at startup; selects production vs. mock based on AI Core binding presence |
+| `FioriRecommendationHandler` | `@After` read handler on all app services (`entity="*"`) — cross-cutting read interceptor; orchestrates the full recommendation pipeline |
+| `RecommendationContextBuilder` | Reads CDS annotations to determine which fields are prediction targets and which columns supply training context |
+| `RptModelSpec` | Static factory for the `ModelDeploymentSpec` targeting `sap-rpt-1-small`; used as the cache key for deployment resolution |
+| `RptInferenceClient` | Calls RPT-1 `/predict` endpoint; handles the synthetic `SAP_RECOMMENDATIONS_ID` index column for composite/non-string keys |
+| `RecommendationResultParser` | Type-coerces RPT-1 string output back to CDS primitive types; resolves `@Common.Text` descriptions from the database |
+| `RecommendationModelChangedHandler` | `@On(EVENT_MODEL_CHANGED)` — invalidates per-tenant entity cache on MTX model upgrade |
+
+### Multi-Tenancy
+
+→ [Multi-Tenancy in cds-feature-ai-core README](../../cds-feature-ai-core/README.md#multi-tenancy)
+
+Tenant isolation is inherited from `cds-feature-ai-core`: each prediction call resolves the resource group and deployment for the current request's tenant. No additional MT configuration is required in this module.
+
+**Per-tenant entity cache** (in `FioriRecommendationHandler`):
+
+```
+Cache<":", Boolean> 10k max, no TTL
+ → entities with no prediction columns are recorded and skipped on every future read
+ → invalidated by RecommendationModelChangedHandler on model change
+```
+
+### Key Flows
+
+#### Recommendation Pipeline (OData GET on draft entity)
+
+```mermaid
+flowchart TD
+ A["OData GET — IsActiveEntity=false"] --> B["FioriRecommendationHandler @After(entity='*') afterRead(...)"]
+ B --> C{Entity in no-prediction cache?}
+ C -->|yes — skip| Z["Return response unchanged"]
+ C -->|no| D{Draft row? Single result?}
+ D -->|no| Z
+ D -->|yes| E["RecommendationContextBuilder: identify prediction fields + context columns"]
+ E --> F{Does this entity have any prediction fields?}
+ F -->|no — add entity to skip cache| Z
+ F -->|yes| G["DB query: up to 2000 context rows (ORDER BY modifiedAt DESC)"]
+ G --> H["cds-feature-ai-core: resolveResourceGroup → resolveDeploymentId → inferenceClient"]
+ H --> I["RptInferenceClient.predict(predictRow, contextRows, columns) POST /v2/inference/deployments/{id}/predict"]
+ I --> J["RecommendationResultParser: type-convert + resolve @Common.Text descriptions"]
+ J --> K["Inject SAP_Recommendations into response row"]
+ K --> L["Return enriched response"]
+```
+
+#### No-prediction-cache Invalidation
+
+```
+ExtensibilityService
+ |
+ | EVENT_MODEL_CHANGED (tenantId)
+ v
+RecommendationModelChangedHandler
+ |
+ | evict all entries in no-prediction-cache for tenantId
+ v
+Next read re-evaluates
+```
+
+---
+
+## Tests
+
+Unit tests for `FioriRecommendationHandler`, `RptInferenceClient`, and `RecommendationConfiguration` live in `src/test/` within this module (`mvn test`).
+
+End-to-end integration tests covering the full recommendation pipeline against a real AI Core instance live in [`integration-tests/`](../../integration-tests/README.md) in the outer module (`RecommendationTest`, `NonStandardKeyRecommendationTest`).
+
+---
+
+## Quality Tools
+
+→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks)