Architecture
Understanding how Da Vinci components work together will help you build better applications.
Package Structure
Da Vinci consists of two Python packages:
- da_vinci (Core Runtime)
Runtime library for application business logic. This is what your application imports and uses at runtime.
- da_vinci_cdk (Infrastructure)
CDK constructs for infrastructure deployment. This generates your CloudFormation templates.
These packages work together but have different purposes and can be versioned independently.
How They Work Together
The key insight is that table definitions are shared between both packages:
You define table objects in your application code
Your application uses these definitions at runtime (via da_vinci)
Your CDK app uses the same definitions to generate infrastructure (via da_vinci_cdk)
This ensures your infrastructure always matches your code.
Runtime Architecture (da_vinci)
The core package provides several key components:
- ORM Layer
TableObject- Base class for table definitionsTableObjectAttribute- Define table attributesTableClient- CRUD operations for tables
- Resource Discovery
ResourceDiscovery- Find AWS resources by nameEliminates hardcoded ARNs
Environment-aware lookups
- Event System
EventPublisher- Publish events to event busEvent-driven architecture support
Type-safe event handling
- Configuration
GlobalSettings- Centralized configurationEnvironment-specific settings
Runtime configuration lookup
- Utilities
Logging helpers
JSON serialization
Exception types
Infrastructure Architecture (da_vinci_cdk)
The CDK package provides constructs for deployment:
- Application Construct
Top-level construct that manages your entire application:
Coordinates multiple stacks
Manages dependencies
Handles cross-stack references
- Table Integration
Automatically creates DynamoDB tables from table object definitions:
Generates table schema
Creates indexes
Sets up permissions
- Stack Management
Handles CloudFormation stack organization and dependencies.
Data Flow
Here’s how data flows through a typical Da Vinci application:
Definition Time
You define table objects:
class UserTable(TableObject): table_name = "users" partition_key_attribute = "user_id" # ...
Deployment Time
CDK generates infrastructure from definitions. Create a stack for your table:
from da_vinci_cdk.stack import Stack from da_vinci_cdk.constructs.dynamodb import DynamoDBTable class UserTableStack(Stack): def __init__(self, app_name, deployment_id, scope, stack_name): super().__init__(app_name, deployment_id, scope, stack_name) self.table = DynamoDBTable.from_orm_table_object( table_object=UserTable, scope=self ) app = Application(app_name="my-app", deployment_id="dev") app.add_uninitialized_stack(UserTableStack) app.synth() # Generates CloudFormation
Runtime
Application uses table client:
client = TableClient(UserTable) user = client.get("user-123")
Component Interaction
Table Object → Infrastructure
Table definitions drive DynamoDB table creation with all attributes, indexes, and settings.
Table Object → Runtime
Same definitions provide type-safe CRUD operations and query building.
Resource Discovery → Services
Services discover each other without hardcoded references:
discovery = ResourceDiscovery() table_name = discovery.get_table_name("users")
Events → Communication
Services communicate via events:
# Service A publishes publisher.publish(event_type="user.created", detail={...}) # Service B consumes (via Lambda trigger) def handler(event, context): # Process event pass
Deployment Model
Da Vinci applications deploy as CloudFormation stacks:
- Single Stack
Simple applications can use a single stack containing all resources.
- Multiple Stacks
Complex applications split into multiple stacks with managed dependencies.
- Cross-Stack References
Da Vinci handles cross-stack references automatically through resource discovery.
Example Architecture
Here’s a typical Da Vinci application architecture:
┌─────────────────────────────────────────────────┐
│ CDK Application │
│ (da_vinci_cdk.Application) │
└─────────────────┬───────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Stack 1 │ │Stack 2 │ │Stack 3 │
│ │ │ │ │ │
│DynamoDB│ │Lambda │ │API GW │
│Tables │ │Functions│ │ │
└────────┘ └────────┘ └────────┘
│ │ │
└────────────┴────────────┘
│
▼
┌─────────────────────────┐
│ Runtime Application │
│ (Uses da_vinci) │
│ │
│ TableClient │
│ ResourceDiscovery │
│ EventPublisher │
└─────────────────────────┘
Best Practices
- Organize by Domain
Group related tables, functions, and resources together.
- Use Resource Discovery
Never hardcode ARNs or resource names. Use ResourceDiscovery to look them up at runtime.
- Centralize Table Definitions
Keep all table definitions in one module that both runtime and CDK code can import.
- Environment Isolation
Use different deployment_id values for different environments (dev, staging, prod).
- Event-Driven Communication
Use events for service-to-service communication rather than direct calls.
- Single Responsibility Stacks
Each stack should have a clear purpose and minimal dependencies.
Extensibility
Da Vinci is designed to be extended:
- Custom Table Clients
Extend TableClient for domain-specific operations.
- Custom CDK Constructs
Mix Da Vinci constructs with your own CDK constructs.
- Custom Events
Define your own event types and handlers.
- Direct AWS Access
Drop down to boto3 when needed for operations not covered by the framework.