Abstract Factory (create matching families together)
When to use
- You need several related objects that must fit together (e.g., storage + secrets for a cloud).
- You want to switch environments (AWS vs GCP vs Local) without changing client code.
- You want tests to swap the whole family (fake storage + fake secrets) at once.
Avoid when you only create one object—use Factory Method or a plain function instead.
Diagram (text)
Client ──> CloudFactory (interface)
├─ storage() → Storage
└─ secrets() → Secrets
▲ ▲
AwsFactory DictSecrets
│ │
S3Store (shared impl)
▲
GcpFactory → GCSStore
Step-by-step idea
- Define tiny product interfaces (here:
Storage,Secrets). - Define a factory interface that builds those products.
- Implement one factory per environment (AWS, GCP, Local).
- Client code asks the factory for products and stays agnostic.
Python example (≤40 lines, type-hinted)
from typing import Protocol
from dataclasses import dataclass
class Storage(Protocol):
def put(self, bucket: str, key: str, data: bytes) -> None: ...
class Secrets(Protocol):
def get(self, name: str) -> str: ...
class CloudFactory(Protocol):
def storage(self) -> Storage: ...
def secrets(self) -> Secrets: ...
@dataclass
class S3Store:
c: object
def put(self, b: str, k: str, d: bytes) -> None: self.c.put_object(Bucket=b, Key=k, Body=d)
@dataclass
class GCSStore:
c: object
def put(self, b: str, k: str, d: bytes) -> None: self.c.upload_blob(b, k, d)
@dataclass
class DictSecrets:
data: dict[str, str]
def get(self, name: str) -> str: return self.data[name]
@dataclass
class AwsFactory:
s3: object; secrets_dict: dict[str, str]
def storage(self) -> Storage: return S3Store(self.s3)
def secrets(self) -> Secrets: return DictSecrets(self.secrets_dict)
@dataclass
class GcpFactory:
gcs: object; secrets_dict: dict[str, str]
def storage(self) -> Storage: return GCSStore(self.gcs)
def secrets(self) -> Secrets: return DictSecrets(self.secrets_dict)
def backup(factory: CloudFactory, data: bytes) -> None:
token = factory.secrets().get("metrics_token")
factory.storage().put("metrics", f"{token}.json", data)
Usage idea
- In prod: pass real clients (
boto3.client("s3"), Google client). - In tests: pass fakes with
put_object(...)/upload_blob(...)methods;backupcode doesn’t change.
Trade-offs & pitfalls
- Pros: Clean environment switching; keeps related objects consistent; test-friendly.
- Cons: More types than a single factory function; some indirection.
- Pitfalls:
- Factories that build too many products—keep the family small and cohesive.
- Mixing vendor details into client code—only the factory/adapters should know vendors.
- Skipping interfaces—use
Protocolso all factories return compatible products.
Pythonic alternatives
- Configuration + registry:
REGISTRY["aws"]()returns a tuple(storage, secrets). - Dataclass constructor injection: build products once and inject them, skip factories entirely.
fsspec+ secret managers: if libraries already unify APIs, a plain function might be enough.
Mini exercise
Add a LocalFactory that returns a FileStore(root_path) + DictSecrets. Make FileStore.put(...) write to disk. Verify backup(LocalFactory(...), b"{}") creates metrics/<token>.json.
Checks (quick checklist)
- Small, clear product interfaces (e.g.,
Storage,Secrets). - One factory per environment returning compatible products.
- Client uses only the factory and product interfaces—no vendor code leaks.
- Tests swap the whole family in one go.
- Keep factories thin; real logic lives in products/adapters.




