Building a Component Registry for Parametric CAD with Python
When building parametric CAD systems, one of the most frustrating challenges is managing components from multiple sources. Your project might need custom-designed parts, standardized fasteners from a library, electronic components for enclosure design, and community-contributed models from a package manager. Without a unified interface, you end up with inconsistent APIs, scattered imports, and code that becomes increasingly difficult to maintain.
This article explores how to solve this problem using the Registry pattern - a design approach that aggregates multiple component sources behind a single, consistent interface. The code examples come from a real-world parametric CAD system built with CadQuery, demonstrating how this pattern handles everything from M3 screws to drone motors to Raspberry Pi boards.
By the end, you will understand how to design a component registry that supports lazy loading, LRU caching, and graceful degradation when optional dependencies are missing.
The problem with multiple component sources
Consider a typical parametric CAD project. You might have:
- Custom components defined in Python files (motors, frames, brackets)
- cq_warehouse providing ISO/DIN-compliant fasteners and bearings
- cq_electronics with PCB footprints and electronic enclosures
- PartCAD as a package manager accessing community-contributed models
Each source has its own API:
# Custom component - function callfrom scripts.components import COMPONENTSmotor = COMPONENTS["motor_2207"]["func"](**COMPONENTS["motor_2207"]["args"])
# cq_warehouse - class instantiationfrom cq_warehouse.fastener import SocketHeadCapScrewscrew = SocketHeadCapScrew(size="M3-0.5", length=10, fastener_type="iso4762")geometry = screw.cq_object
# cq_electronics - different return typesfrom cq_electronics.boards import RPi3brpi = RPi3b() # Returns Assembly, not Workplane
# PartCAD - hierarchical paths and context managementimport partcad as pcctx = pc.init()bolt = ctx.get_part("//pub/std/metric/cqwarehouse:fastener/hexhead-iso4017")This inconsistency creates several problems:
- Cognitive overhead - developers must remember different APIs for each source
- Tight coupling - code depends directly on specific libraries
- No caching - repeated access to the same component rebuilds geometry
- Brittle dependencies - missing optional libraries break the entire application
Designing the ComponentSource abstraction
The first step is defining an abstract interface that all component sources must implement. This follows the Dependency Inversion Principle: depend on abstractions, not concrete implementations.

from abc import ABC, abstractmethodfrom collections.abc import Iteratorfrom dataclasses import dataclassfrom typing import Any
@dataclassclass ComponentSpec: """Metadata about a component without geometry.""" name: str source: str category: str params: dict[str, Any] description: str = ""
class ComponentSource: """Abstract interface for component sources."""
@property def name(self) -> str: """Source identifier (e.g., 'cq_warehouse', 'custom').""" raise NotImplementedError
def list_components(self) -> Iterator[ComponentSpec]: """Yield all available component specs from this source.""" raise NotImplementedError
def get_component(self, name: str, **params: Any) -> Component: """Load a component by name with optional parameters.""" raise NotImplementedError
def search(self, query: str) -> Iterator[ComponentSpec]: """Search components by name/description.""" query_lower = query.lower() for spec in self.list_components(): if query_lower in spec.name.lower() or query_lower in spec.description.lower(): yield specThe ComponentSpec dataclass separates metadata from geometry - you can browse the catalog without loading heavy 3D models. The default search() implementation provides basic filtering that sources can override for more sophisticated queries.
Implementing source adapters
Each component source needs an adapter that implements the ComponentSource interface. Here is the adapter for cq_warehouse fasteners:
from semicad.core.component import Component, ComponentSpecfrom semicad.core.registry import ComponentSource
FASTENER_CLASSES = { "SocketHeadCapScrew": ("iso4762", "Socket head cap screw (ISO 4762)"), "ButtonHeadScrew": ("iso7380_1", "Button head screw (ISO 7380-1)"), "HexHeadScrew": ("iso4014", "Hex head screw (ISO 4014)"), "HexNut": ("iso4032", "Hex nut (ISO 4032)"),}
class WarehouseSource(ComponentSource): """Source for cq_warehouse components."""
def __init__(self) -> None: self._fasteners: dict[str, tuple[type, str, str]] = {} self._load_fasteners()
@property def name(self) -> str: return "cq_warehouse"
def _load_fasteners(self) -> None: """Load fastener classes from cq_warehouse.""" try: from cq_warehouse import fastener as f for class_name, (default_type, desc) in FASTENER_CLASSES.items(): if hasattr(f, class_name): cls = getattr(f, class_name) self._fasteners[class_name] = (cls, default_type, desc) except ImportError: pass # cq_warehouse not installed
def get_component(self, name: str, **params: Any) -> Component: if name in self._fasteners: cls, default_type, desc = self._fasteners[name] size = params.get("size", "M3-0.5") length = params.get("length") fastener_type = params.get("fastener_type", default_type)
spec = ComponentSpec( name=f"{name}_{size}_{length}mm" if length else f"{name}_{size}", source=self.name, category="fastener", description=desc, ) return WarehouseFastener(spec, cls, fastener_type, size, length)
raise KeyError(f"Component not found in cq_warehouse: {name}")The adapter handles the translation between our unified interface and cq_warehouse’s specific API. The try/except ImportError pattern allows the system to work even when cq_warehouse is not installed.
The central registry with LRU caching
The ComponentRegistry class aggregates all sources and provides caching for frequently accessed components:

from collections import OrderedDict
def _make_cache_key(name: str, params: dict[str, Any]) -> str: """Generate a deterministic cache key from name and parameters.""" if not params: return name sorted_items = sorted(params.items()) params_str = ",".join(f"{k}={v!r}" for k, v in sorted_items) return f"{name}:{params_str}"
class ComponentRegistry: """Central registry for all component sources."""
DEFAULT_CACHE_SIZE = 128
def __init__(self, cache_size: int | None = None): self._sources: dict[str, ComponentSource] = {} self._cache: OrderedDict[str, Component] = OrderedDict() self._cache_max_size = cache_size or self.DEFAULT_CACHE_SIZE self._cache_hits = 0 self._cache_misses = 0
def register_source(self, source: ComponentSource) -> None: """Register a new component source.""" self._sources[source.name] = source
def get(self, full_name: str, use_cache: bool = True, **params: Any) -> Component: """Get component by name with optional caching.""" cache_key = _make_cache_key(full_name, params)
if use_cache and cache_key in self._cache: self._cache_hits += 1 self._cache.move_to_end(cache_key) # LRU ordering return self._cache[cache_key]
self._cache_misses += 1 component = self._get_uncached(full_name, **params)
if use_cache: self._cache[cache_key] = component self._cache.move_to_end(cache_key) while len(self._cache) > self._cache_max_size: self._cache.popitem(last=False) # Evict oldest
return componentThe caching strategy uses Python’s OrderedDict to implement LRU (Least Recently Used) eviction. Cache keys incorporate both the component name and parameters, ensuring that get("SocketHeadCapScrew", size="M3-0.5", length=10) caches separately from get("SocketHeadCapScrew", size="M4-0.7", length=16).
Pro Tip: The
use_cache=Falseparameter allows forcing a fresh instance when needed, useful for testing or when geometry must not be shared between assemblies.
Graceful degradation with optional dependencies
The registry initialization demonstrates how to handle optional dependencies gracefully:
def _init_default_sources(registry: ComponentRegistry) -> None: """Initialize registry with default sources.""" from semicad.sources import custom, electronics, partcad_source, warehouse
# Always available - built-in components try: registry.register_source(custom.CustomSource()) except (ImportError, OSError, RuntimeError) as e: logging.debug("Custom source not available: %s", e)
# Optional - requires cq_warehouse try: registry.register_source(warehouse.WarehouseSource()) except ImportError: pass # Expected if not using fasteners
# Optional - requires cq_electronics try: registry.register_source(electronics.ElectronicsSource()) except ImportError: pass # Expected if not using electronics
# Optional - requires partcad try: registry.register_source(partcad_source.PartCADSource()) except ImportError: pass # Expected if not using package managerThis pattern ensures the registry works with any combination of installed dependencies. A minimal installation with only CadQuery can still use custom components. Adding cq_warehouse enables fasteners. The system never crashes due to missing optional libraries.
Practical usage examples
With the registry in place, accessing any component uses the same interface:
from semicad import get_registry
registry = get_registry()
# Custom drone motormotor = registry.get("motor_2207")motor_geometry = motor.geometry # Lazy-loads on first access
# ISO 4762 socket head cap screw (M3 x 10mm)screw = registry.get("SocketHeadCapScrew", size="M3-0.5", length=10)
# Raspberry Pi 3B for enclosure designrpi = registry.get("RPi3b")print(rpi.dimensions) # (85.0, 56.0, 1.5)print(rpi.mounting_holes) # [(24.5, 19.0), (-24.5, -39.0), ...]
# PartCAD community modelbolt = registry.get( "//pub/std/metric/cqwarehouse:fastener/hexhead-iso4017", size="M10-1.5", length=30)
# Search across all sourcesfor spec in registry.search("motor"): print(f"{spec.source}/{spec.category}/{spec.name}")The Component class provides lazy geometry loading - the 3D model is only built when you access the geometry property. This means browsing the catalog is fast, and expensive CAD operations are deferred until actually needed.
Cache performance monitoring
The registry tracks cache statistics to help tune performance:
from dataclasses import dataclass
@dataclassclass CacheStats: """Statistics about component cache usage.""" hits: int misses: int size: int max_size: int
@property def hit_rate(self) -> float: """Cache hit rate as a percentage.""" total = self.hits + self.misses return (self.hits / total * 100) if total > 0 else 0.0
# Monitor cache performancestats = registry.cache_stats()print(f"Cache hit rate: {stats.hit_rate:.1f}%")print(f"Cache size: {stats.size}/{stats.max_size}")
# Clear cache when memory constrainedcleared = registry.clear_cache()print(f"Cleared {cleared} cached components")In practice, a cache size of 128 components works well for most projects. Assemblies that repeatedly access the same fastener (think: 20 identical screws) see significant speedups from caching.
Architecture overview
The complete architecture follows a layered design:
┌─────────────────────────────────────────────────────────┐│ Application Code ││ registry.get("motor_2207") │└─────────────────────────┬───────────────────────────────┘ │┌─────────────────────────▼───────────────────────────────┐│ ComponentRegistry ││ - Aggregates sources - LRU caching ││ - Unified get() API - Search across all │└───────┬─────────┬─────────┬─────────┬───────────────────┘ │ │ │ │┌───────▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───────┐│ Custom │ │ Ware- │ │ Elec- │ │ PartCAD ││ Source │ │ house │ │ tronics│ │ Source ││ │ │ Source│ │ Source │ │ │└───────────┘ └───────┘ └────────┘ └───────────┘ │ │ │ │┌────▼────┐ ┌─────▼────┐ ┌───▼────┐ ┌────▼──────┐│scripts/ │ │cq_ware- │ │cq_elec-│ │Community ││compo- │ │house │ │tronics │ │CAD Repo ││nents.py │ │library │ │library │ │(network) │└─────────┘ └──────────┘ └────────┘ └───────────┘Each layer has a single responsibility:
- Application code uses the unified
get()API - ComponentRegistry handles caching and source aggregation
- Source adapters translate between the registry interface and specific libraries
- Underlying libraries provide the actual CAD geometry
Key takeaways
Building a component registry for parametric CAD taught several valuable lessons:
- The Registry pattern provides a clean solution for aggregating multiple component sources behind a unified interface
- Lazy loading defers expensive geometry generation until actually needed, keeping catalog browsing fast
- LRU caching with deterministic keys (incorporating parameters) significantly speeds up assemblies with repeated components
- Graceful degradation through try/except around source initialization allows the system to work with any subset of optional dependencies
- Separating specs from geometry via
ComponentSpecenables fast catalog browsing without loading 3D models
The pattern scales well - adding a new component source requires only implementing the ComponentSource interface. The registry, caching, and search functionality work automatically with any conforming source.
For projects combining custom designs with standard parts libraries, this architecture eliminates the cognitive overhead of multiple APIs while providing performance optimizations that matter for complex assemblies.