# Architecture Conventions

## Overview

The SDK follows a layered architecture with clear separation of concerns and explicit dependencies.

## Layering

### Three-Layer Architecture

The SDK is organized into three distinct layers:

**Layer 1: Public API** (`__init__.py`)
- User-facing interface
- Clean, stable exports

**Layer 2: Protocol Layer** (`__interface__/`)
- Type contracts
- Interface definitions

**Layer 3: Implementation** (`_internal/`)
- Internal details
- Concrete implementations

### Layer Responsibilities

**Public API** (`pinecone/__init__.py`)
- Exports only stable, user-facing APIs
- Re-exports from `_internal/` implementations
- Provides top-level client construction
- **Never** contains implementation logic

**Protocol Layer** (`pinecone/__interface__/`)
- Defines type protocols for all public interfaces
- Uses `typing.Protocol` with `@runtime_checkable`
- Specifies method signatures, return types, properties
- No implementation code, only specifications

> **Note**: Use `@protocol-compliance-review` agent to verify implementations match their protocols.

**Implementation Layer** (`pinecone/_internal/`)
- Contains all implementation details
- Implements protocols from `__interface__`
- Handles HTTP, serialization, error handling
- Not directly importable by users

## Module Organization

### Directory Structure

```
pinecone/
├── __init__.py                  # Public exports only
├── _client.py                   # Pinecone class (sync control plane client)
├── _async_client.py             # AsyncPinecone class (async control plane client)
├── __interface__/               # Protocol definitions
│   ├── __init__.py
│   ├── client.py               # Client protocols
│   ├── indexes/                # Index namespace protocols
│   ├── collections/            # Collections namespace protocols
│   ├── inference/              # Inference namespace protocols
│   ├── backups/                # Backups namespace protocols
│   └── index.py                # Data plane protocols
├── client/                      # Control plane namespaces (accessed via Pinecone/AsyncPinecone)
│   ├── __init__.py
│   ├── collections.py          # Collections namespace implementation
│   ├── async_collections.py    # AsyncCollections namespace implementation
│   ├── backups.py              # Backups namespace implementation
│   ├── async_backups.py        # AsyncBackups namespace implementation
│   ├── inference.py            # Inference namespace implementation
│   ├── async_inference.py      # AsyncInference namespace implementation
│   ├── indexes.py              # Indexes namespace implementation
│   ├── async_indexes.py        # AsyncIndexes namespace implementation
│   ├── restore_jobs.py         # RestoreJobs namespace implementation
│   └── async_restore_jobs.py   # AsyncRestoreJobs namespace implementation
├── index/                       # Data plane client & sub-namespaces (accessed via Index)
│   ├── __init__.py             # Index class (sync data plane client)
│   ├── bulk_imports.py         # BulkImports sub-namespace implementation
│   ├── namespaces.py           # Namespaces sub-namespace implementation
│   ├── records.py              # Records sub-namespace implementation
│   └── documents.py            # Documents sub-namespace implementation
├── async_index/                 # Async data plane client & sub-namespaces (accessed via AsyncIndex)
│   ├── __init__.py             # AsyncIndex class (async data plane client)
│   ├── bulk_imports.py         # AsyncBulkImports sub-namespace (no async_ prefix)
│   ├── namespaces.py           # AsyncNamespaces sub-namespace (no async_ prefix)
│   ├── records.py              # AsyncRecords sub-namespace (no async_ prefix)
│   └── documents.py            # AsyncDocuments sub-namespace (no async_ prefix)
├── _internal/                   # Internal implementation details
│   ├── http/                   # HTTP client layer
│   │   ├── client.py
│   │   ├── async_client.py
│   │   ├── config.py
│   │   └── base.py
│   ├── adapters/               # API version adapters
│   │   ├── indexes_adapter.py
│   │   ├── inference_adapter.py
│   │   ├── collections_adapter.py
│   │   └── backups_adapter.py
│   ├── constants.py            # Shared constants (API versions, etc.)
│   └── validation.py           # Input validation utilities
└── py.typed                    # PEP 561 marker
```

**`constants.py`**: Store SDK-wide constant values like API versions, header names, and magic strings that need to be shared across sync and async implementations. Centralizing these prevents drift and makes updates easier. See [async.md](./async.md#shared-constants) for details.

### Namespace Implementation Organization

Namespace implementations are organized by their parent client class:

**Control plane namespaces** (accessed via `Pinecone` / `AsyncPinecone`):
```
**Modern implementation pattern:**
```python
# pinecone/client/indexes.py
class Indexes(IndexesNamespaceProtocol):
    def __init__(self, http_client: HTTPClient, base_url: str):
        self._http = http_client
        self._base_url = base_url
        self._adapter = IndexesAdapter()

    def create(
        self,
        name: str,
        *,
        dimension: int,
        metric: str = "cosine",
    ) -> None:
        """Create an index.

        Args:
            name: Index name
            dimension: Vector dimension
            metric: Distance metric

        Example:
            >>> pc.indexes.create(name="my-index", dimension=1536)
        """
        # Implementation directly in method
        require_non_empty("name", name)
        require_positive("dimension", dimension)

        request = CreateIndexRequest(name=name, dimension=dimension, metric=metric)
        response = self._http.post(
            f"{self._base_url}/indexes",
            json=msgspec.json.encode(request),
        )
        # ... rest of implementation

    def list(self, *, limit: int = 100) -> Paginator[IndexModel]:
        """List all indexes."""
        # ... implementation
```

**Benefits of this pattern:**
- **Full type hints** - IDEs show complete parameter names, types, and docstrings on hover
- **No `type: ignore` needed** - Static analysis works correctly without suppression
- **Simpler architecture** - No dynamic binding complexity, easier to understand for newcomers
- **Direct navigation** - "Go to definition" takes you to the actual implementation
- **Better error messages** - Stack traces show actual method locations

**Tradeoffs:**
- **Larger files** - Single namespace file contains all methods (200-700 lines typical)
- **Potential merge conflicts** - Multiple developers editing different methods in same file
- **But:** Modern IDEs handle large files well, and improved UX outweighs organizational concerns

### Model Organization

Models use a **subpackage-per-namespace** structure to reduce merge conflicts:

```
pinecone/models/
├── __init__.py                    # Re-exports all public models
├── _display.py                    # Shared display utilities
├── pagination.py                  # Pagination infrastructure
│
├── inference/                     # Inference namespace models
│   ├── __init__.py               # Lazy loading with __getattr__
│   ├── _common.py                # Shared models (Usage)
│   ├── embed.py                  # Embedding models
│   ├── rerank.py                 # Reranking models
│   └── list_models.py            # Model listing models
│
├── indexes/                       # Index namespace models
│   ├── __init__.py               # Lazy loading with __getattr__
│   ├── _common.py                # IndexStatus
│   ├── specs.py                  # ServerlessSpec, PodSpec, etc.
│   ├── index.py                  # IndexModel, IndexConfig
│   └── list.py                   # IndexList
│
├── backups/                       # Backup namespace models
│   ├── __init__.py               # Lazy loading with __getattr__
│   ├── backup.py                 # BackupModel
│   └── create.py                 # CreateBackupRequest
│
└── collections/                   # Collection namespace models
    ├── __init__.py               # Lazy loading with __getattr__
    ├── collection.py             # CollectionModel
    ├── list.py                   # CollectionList
    └── create.py                 # CreateCollectionRequest
```

**Design principles:**
- **One file per operation** - Reduces merge conflicts when multiple developers work on different methods
- **Shared models in `_common.py`** - Cross-cutting models like `Usage` and `Status`
- **Lazy loading via `__getattr__`** - Only loads models actually used, maintains import performance (~2-3ms per model)
- **Backward compatibility** - All models remain importable from namespace package: `from pinecone.models.inference import EmbedRequest`

**Lazy loading pattern (PEP 562):**
```python
# pinecone/models/inference/__init__.py
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from pinecone.models.inference.embed import EmbedRequest

_LAZY_IMPORTS = {
    "EmbedRequest": "pinecone.models.inference.embed",
}

def __getattr__(name: str) -> Any:
    """Lazy-load models on first access."""
    if name in _LAZY_IMPORTS:
        from importlib import import_module
        module = import_module(_LAZY_IMPORTS[name])
        return getattr(module, name)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
```

## Dependency Rules

### Layer Dependencies

```
Public API ──→ Implementation
              ↓
          Protocols ←── Implementation
```

**Rules**:
1. Public API depends on Implementation (for exports)
2. Implementation depends on Protocols (for type contracts)
3. Protocols depend on nothing internal (only stdlib)
4. **Never** circular dependencies between modules

### Import Patterns

**✅ Good: Public API imports from internal**

```python
# pinecone/__init__.py
from pinecone._internal.client.client import Client
from pinecone._internal.errors import PineconeError

__all__ = ["Client", "PineconeError"]
```

**✅ Good: Implementation implements protocol**

```python
# pinecone/_internal/client/client.py
from pinecone.__interface__.client import ClientProtocol

class Client:
    """Implements ClientProtocol."""
    ...
```

**❌ Bad: Protocol imports from internal**

```python
# pinecone/__interface__/client.py
from pinecone._internal.models import Config  # NEVER!
```

**❌ Bad: Circular imports**

```python
# pinecone/_internal/client.py
from pinecone._internal.indexes import IndexManager  # If IndexManager imports Client
```

## Module Boundaries

### Namespace Organization

Each major feature gets its own namespace:

```python
pc = Pinecone(api_key="...")

# Control plane namespaces (on Pinecone client)
pc.indexes              # Index management (create, list, delete, describe)
pc.collections          # Collection management
pc.inference            # Embedding/reranking

# Data plane client (standalone, constructed with host)
index = pc.index(host="my-index-abc123.svc.pinecone.io")
index.upsert(...)              # Core vector operations (top-level)
index.query(...)
index.bulk_imports.start(...)  # Sub-namespaces for related operations
index.namespaces.list()
index.records.search(...)
```

### Cross-Module Communication

**Via Shared HTTP Client**:

```python
# All modules receive the same HTTPClient instance
class Client:
    def __init__(self, api_key: str):
        self._http = HTTPClient(api_key=api_key)
        self.indexes = IndexManager(self._http)
        self.inference = Inference(self._http)
```

**Via Dependency Injection**:

```python
class IndexManager:
    def __init__(self, http_client: HTTPClient):
        self._http = http_client
```

## Principles

### 1. Minimize Public Surface

**Principle**: Only expose what users need.

**Example**:
```python
# ✅ Good: Minimal public API
__all__ = ["Client", "PineconeError"]

# ❌ Bad: Exposing internals
__all__ = ["Client", "HTTPClient", "RateLimiter", "parse_response"]
```

### 2. Hide Implementation Details

**Principle**: Internal modules use `_internal` prefix.

**Example**:
```python
# ✅ Good: Users cannot accidentally import internals
from pinecone._internal.http.client import HTTPClient  # Not in __all__

# ❌ Bad: Internal details in public namespace
from pinecone.http.client import HTTPClient  # Looks public!
```

### 3. Use Protocols for Contracts

**Principle**: Protocols define interfaces, classes implement them.

**Example**:
```python
# ✅ Good: Protocol defines contract
@runtime_checkable
class ClientProtocol(Protocol):
    def list_indexes(self) -> list[str]: ...

class Client:
    def list_indexes(self) -> list[str]:
        # Implementation
        ...

# ❌ Bad: No protocol, just implementation
class Client:
    def list_indexes(self) -> list[str]:
        ...
```

### 4. One Concept Per Module

**Principle**: Each module has a single clear responsibility.

**Example**:
```python
# ✅ Good: Focused modules
_internal/http/client.py        # HTTP requests only
_internal/http/rate_limiter.py  # Rate limiting only
_internal/errors.py             # Exception hierarchy only

# ❌ Bad: Mixed concerns
_internal/utils.py              # Everything mixed together
```

## Config Parameter Alignment

### Rule

The `Pinecone`, `AsyncPinecone`, `Index`, and `AsyncIndex` classes accept the same shared configuration parameters. When adding a new shared config param, update all four constructors.

**Shared parameters** (mirrored across all client classes):
- `api_key`
- `timeout`
- `additional_headers`

**Class-specific parameters:**
- `Pinecone` / `AsyncPinecone`: `base_url` (control plane endpoint)
- `Index` / `AsyncIndex`: `host` (data plane endpoint)

### Factory Forwarding

The `pc.index()` factory method forwards all shared config from the `Pinecone` client so users don't repeat configuration:

```python
pc = Pinecone(api_key="...", timeout=60)

# Factory forwards api_key, timeout, additional_headers automatically
index = pc.index(host="my-index-abc123.svc.pinecone.io")
```

Internally, both `Pinecone` and `Index` construct their own `HTTPConfig` from the same set of values. The logic isn't duplicated -- just the constructor signatures are aligned.

### Standalone Construction

`Index` is a first-class object that can be constructed directly without a `Pinecone` client:

```python
from pinecone import Index

index = Index(host="my-index-abc123.svc.pinecone.io", api_key="...")
```

This is useful when you only need data plane operations and don't need index management. The `Index` creates its own `HTTPClient` pointed at the index host.

## Anti-Patterns

### ❌ God Objects

**Problem**: One class doing too many things.

```python
# Bad: Client does everything
class Client:
    def list_indexes(self): ...
    def create_index(self): ...
    def embed(self): ...
    def upsert(self): ...
    # 50+ methods...
```

**Solution**: Separate namespaces.

```python
# Good: Separate concerns
class Client:
    @property
    def indexes(self) -> IndexManager: ...

    @property
    def inference(self) -> Inference: ...

class IndexManager:
    def list(self): ...
    def create(self): ...

class Inference:
    def embed(self): ...
```

### ❌ Leaky Abstractions

**Problem**: Internal details exposed in public API.

```python
# Bad: Exposing HTTPClient
def list_indexes(self) -> list[str]:
    response = self._http.get("/indexes")  # http details leaked
    return response.json()
```

**Solution**: Hide implementation details.

```python
# Good: Return clean data models
def list_indexes(self) -> list[IndexInfo]:
    response = self._http.get("/indexes")
    return [IndexInfo.from_json(item) for item in response.json()]
```

### ❌ Circular Dependencies

**Problem**: Modules importing each other.

```python
# Bad: Circular import
# client.py
from pinecone._internal.indexes import IndexManager

# indexes.py
from pinecone._internal.client import Client  # Circular!
```

**Solution**: Use dependency injection.

```python
# Good: Inject dependencies
# client.py
class Client:
    def __init__(self):
        self._http = HTTPClient()
        self.indexes = IndexManager(self._http)  # Pass dependency

# indexes.py
class IndexManager:
    def __init__(self, http_client: HTTPClient):  # Receive dependency
        self._http = http_client
```

## References

- See [naming.md](./naming.md) for module and class naming conventions
- See [types.md](./types.md) for protocol definition patterns
- See [testing.md](./testing.md) for testing layer boundaries
- See [http-client.md](./http-client.md) for HTTP layer architecture
