Documentation
System architecture
OpenDDE is a microservices platform orchestrated by Docker Compose. Six containers work together to provide a complete computational drug design workflow.
System diagram
┌─────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Frontend │───▶│ Backend │───▶│ Microservices │ │
│ │ Next.js │ │ FastAPI │ │ │ │
│ │ :3000 │ │ :8000 │ │ P2Rank :5001 │ │
│ └──────────┘ │ │ │ RDKit :5002 │ │
│ │ │───▶│ Immune :5003 │ │
│ │ │ │ │ │
│ │ │ └──────────────────┘ │
│ │ │ │
│ │ │───▶ Redis :6379 │
│ │ │───▶ Supabase (external) │
│ │ │───▶ ChEMBL API (external) │
│ │ │───▶ UniProt API (external) │
│ │ │───▶ Claude API (external) │
│ └──────────┘ │
└─────────────────────────────────────────────────────────┘Services
| Service | Port | Technology | Purpose |
|---|---|---|---|
| frontend | 3000 | Next.js 14 | React UI with App Router |
| backend | 8000 | FastAPI + Python | REST API, orchestration, caching |
| p2rank | 5001 | Java + Flask wrapper | ML pocket prediction |
| rdkit | 5002 | Python + RDKit | Molecular properties, depiction, similarity |
| immunebuilder | 5003 | Python + PyTorch | Antibody structure prediction |
| redis | 6379 | Redis 7 | Response caching, session data |
Data flow
Here’s what happens when you search for a protein target:
- Frontend sends
POST /api/v1/target/resolvewith the UniProt ID or gene name. - Backend checks Supabase for a cached target. If not found, it queries the UniProt API and downloads the AlphaFold structure (CIF file).
- Backend sends the CIF file to the P2Rank service, which returns predicted binding pockets.
- Backend queries ChEMBL for known ligands targeting this protein.
- Backend stores everything in Supabase and returns the full target payload to the frontend.
- Frontend renders the 3D structure, pocket list, and ligand table.
Engine swap layer
OpenDDE is designed so that any computational engine can be replaced without changing the rest of the system. Each engine is accessed through a standardized adapter interface:
# backend/engines/pocket_engine.py
class PocketEngine:
"""Abstract interface for pocket prediction engines."""
async def predict(self, structure_path: str) -> list[Pocket]:
raise NotImplementedError
class P2RankEngine(PocketEngine):
"""P2Rank implementation."""
async def predict(self, structure_path: str) -> list[Pocket]:
response = await httpx.post(
f"{self.base_url}/predict",
files={"structure": open(structure_path, "rb")}
)
return self._parse_response(response.json())
# Future: swap in a different engine
class FPocketEngine(PocketEngine):
"""Alternative pocket predictor."""
...This modular design means you could swap P2Rank for FPocket, AlphaFold for Boltz-2, or ImmuneBuilder for ABodyBuilder3 — all by implementing a single adapter class. See Engine swap layer for details.
Caching strategy
OpenDDE uses two layers of caching:
- In-memory response cache — GET requests to
/api/v1/*are cached in memory for 1 hour (max 200 entries). Returnsx-cache: HITorx-cache: MISSheaders. - Structure file caching — CIF files are served with
Cache-Control: public, max-age=86400(24 hours).