Skip to content

01 · Enterprise RAG Architecture Patterns

Everything through Level 3 is one pipeline, one corpus, one set of users. An enterprise deployment usually has none of those luxuries: multiple teams with different corpora, different access rules, different latency and cost budgets, and a platform team that has to support all of them without rebuilding the pipeline per team. This module is architectural — comparing the patterns enterprises actually converge on — with runnable Python only where the trade-off is genuinely a code-level decision (routing logic, config-driven pipelines); the rest is deliberate, stated manual review, since "which architecture fits your org" isn't something a toy script can validate.

Three patterns, and when each wins

1. Single shared index, metadata-filtered per tenant/team. One vector store, one ingestion pipeline; every document is tagged with team_id or tenant_id, and every query filters on it. Cheapest to operate, and the riskiest — a missing or wrong filter leaks data across tenants (Level 4 module 2 covers this failure mode exhaustively). Fits well when tenants are internal teams with moderate trust and a platform team enforcing filters centrally.

2. Index-per-tenant. Separate vector collections (or separate database instances) per tenant. No shared-filter risk by construction — a bug can't leak tenant A's data into tenant B's query because there's no code path that touches both. Costs more operationally: N tenants means N indexes to provision, monitor, and keep fresh, and small tenants pay a fixed per-index overhead disproportionate to their data volume.

3. Hub-and-spoke with a shared retrieval service. Teams own their data and ingestion; a central service exposes a common retrieval API, handles auth, routing, and observability uniformly, and enforces org-wide policies (module 05, 06) in one place instead of per-team reimplementation. This is the pattern most enterprises grow into after starting with pattern 1, because it's the only one of the three that scales the platform team's effort sub-linearly with the number of internal RAG consumers.

Shared index + filters Index-per-tenant Hub-and-spoke
Isolation guarantee Enforced in query code Structural Enforced in gateway code
Ops overhead Lowest Scales with tenant count Moderate, centralized
Best for Few, trusted internal teams Regulated/high-trust-boundary tenants Many teams, shared platform investment
Biggest risk Filter bug = data leak Cost sprawl, index drift Gateway becomes a bottleneck/SPOF

Config-driven pipelines: the part that is actually code

What is a concrete, runnable engineering decision is whether each team's pipeline is a bespoke script or a declarative config against a shared engine. The shared-engine version scales a platform team's effort; the bespoke version scales the number of pipelines someone has to individually understand during an incident.

from dataclasses import dataclass, field

@dataclass
class PipelineConfig:
    team_id: str
    chunk_size: int = 512
    chunk_overlap: int = 64
    top_k: int = 5
    embedding_model: str = "default-small"
    rerank: bool = False
    allowed_sources: list = field(default_factory=list)

def build_pipeline(config: PipelineConfig):
    # A real version wires this into your retriever/generator factory.
    # Returning a summary dict here to make the routing decision inspectable.
    return {
        "team_id": config.team_id,
        "index_name": f"idx_{config.team_id}",
        "chunking": (config.chunk_size, config.chunk_overlap),
        "retrieval": {"top_k": config.top_k, "rerank": config.rerank},
        "embedding_model": config.embedding_model,
    }

legal_team = PipelineConfig(team_id="legal", chunk_size=256, rerank=True,
                             allowed_sources=["contracts", "policies"])
support_team = PipelineConfig(team_id="support", chunk_size=512, top_k=8)

for cfg in (legal_team, support_team):
    print(build_pipeline(cfg))

Captured output:

{'team_id': 'legal', 'index_name': 'idx_legal', 'chunking': (256, 64), 'retrieval': {'top_k': 5, 'rerank': True}, 'embedding_model': 'default-small'}
{'team_id': 'support', 'index_name': 'idx_support', 'chunking': (512, 64), 'retrieval': {'top_k': 8, 'rerank': False}, 'embedding_model': 'default-small'}

One build_pipeline function serves both teams' different needs from data, not from two different code paths — this is the concrete mechanism behind "hub-and-spoke scales platform effort sub-linearly": adding team 51 is a new PipelineConfig, not a new pipeline implementation to maintain.

The trap: architecture decisions made by whoever asked first

The single most common enterprise RAG failure isn't a bug — it's pattern 1 adopted implicitly, one team at a time, with no one deciding it deliberately. Team A builds a quick shared index for their docs. Team B asks to add theirs "to the same thing, it's already there." Six months in, there's a shared index with a dozen teams' data, filter logic that's grown ad hoc per team, and no one who chose pattern 1 as a policy — it just accreted. By the time someone asks "wait, can tenant X's queries actually see tenant Y's data?" the answer is usually "we're not sure," which is the module 02 scenario this level exists to prevent.

The fix isn't picking the "best" pattern up front — it's making the choice explicit and reviewed before the second team joins a shared resource, with the isolation guarantee (structural vs. enforced-in-code) stated in writing, because that's the fact that determines how carefully every future filter change has to be reviewed.

Cheat sheet

Signal Suggests
2-3 internal teams, similar trust level Shared index + filters is fine, keep filter logic centralized
Regulatory separation required (e.g. GDPR data residency per customer) Index-per-tenant, or physically separate infra
10+ teams, growing Hub-and-spoke — invest in the shared retrieval service
"Just add it to the existing index" without a filter review Stop — this is how implicit pattern-1 sprawl starts
One team's config change breaks another team's pipeline Sign the config-driven approach was needed and isn't there yet

How It Actually Works

Why architecture pattern choice is really a choice about where the embed→retrieve→generate seam gets duplicated or shared. Level-1 lesson 7 established that every RAG pipeline decomposes into the same ingestion and query phases regardless of scale. Enterprise patterns differ only in how many independent copies of that pipeline exist and how they share infrastructure: a single shared index serving many teams keeps one embedding model, one vector store, and one set of chunking rules consistent across the org (cheap to operate, but every team's documents compete for the same top_k slots and the same relevance tuning); federated per-team indexes each get pipeline decisions tuned to their own corpus (a legal team's chunking needs differ genuinely from an engineering wiki's, lesson 3's tradeoffs are not universal) at the cost of N times the operational surface area and no cross-team retrieval without an extra fan-out-and-fuse layer (level-2 lesson 2's RRF, applied across indexes instead of across techniques within one).

Why config-driven pipelines are the practical answer to "the pipeline keeps changing." Every lesson from chunking through reranking exposed parameters (chunk size, top_k, fusion weights, rerank candidate count) that this course tuned by hand per lesson — at enterprise scale, with multiple corpora and teams, those parameters need to be re-tunable per deployment without a code change and re-deploy, because different corpora genuinely warrant different settings (level-3 lesson 5's point that benchmark results don't transfer across domains applies to pipeline parameters just as much as to embedding model choice). Externalizing them into config makes re-tuning an operational change instead of an engineering one — which is also precisely what makes "architecture decisions made by whoever asked first" a real trap: a config default set for one team's corpus silently becomes every other team's default too, unless the config-driven system is deliberately built to vary per tenant rather than globally.

Exercise

Extend PipelineConfig with an isolation_mode field ("shared_filtered", "per_tenant", or "hub_spoke"), and write a validate_config function that raises if isolation_mode == "shared_filtered" but allowed_sources is empty — treating "no explicit source restriction on a shared index" as a configuration error rather than a default to silently allow. This is the kind of guardrail-as-code that turns an implicit architecture decision into an enforced one.