Appearance
Custom Resolvers
About This Guide
This document helps you extend DDK-generated servers with custom business logic that goes beyond standard CRUD operations. It focuses on how to add specialized queries, mutations, and integrations while preserving your custom code across schema regenerations.
Who this is for: Backend developers and engineers who need to implement business logic, complex workflows, or external integrations on top of DDK-generated servers.
What you'll learn:
- When and why to use custom resolvers
- How to define custom operations in your schema
- How custom code is preserved during regeneration
- Patterns for authentication, pagination, aggregation, and external integrations
- Best practices for testing and error handling
What we don't cover: To keep this guide clear and accessible, we don't cover the internal details of the DDK file generation system, the complete generated resolver implementation, or proprietary optimization techniques. For advanced customization scenarios beyond the documented patterns, please contact our support team.
This guide covers how to extend DDK-generated servers with custom business logic beyond standard database operations.
Table of Contents
- When to Use Custom Resolvers
- Custom Resolver Workflow
- Capabilities
- Preservation Across Regeneration
- Best Practices
- Common Patterns
- Testing Approaches
When to Use Custom Resolvers
The DDK auto-generates standard create, read, update, and delete (CRUD) operations for your database entities. Custom resolvers extend beyond these operations to handle specialized business logic that doesn't fit the standard CRUD pattern.
The DDK's custom resolver system solves a fundamental code generation problem: how to continuously regenerate your database layer while preserving custom business logic you've added. The solution uses file naming conventions—custom resolver files are never overwritten by the generator, while generated files can be safely regenerated at any time.
Complex Business Logic
Custom resolvers enable multi-step workflows, complex validation rules, conditional processing, and business rule enforcement that go beyond simple database operations.
Examples:
- Order processing workflows with inventory checks and payment validation
- User registration with email verification and account setup
- Complex approval workflows with multiple stakeholder checks
Advanced Queries
Handle queries that span multiple database tables, perform aggregations and analytics, or require complex filtering conditions.
Examples:
- Dashboard analytics combining data from multiple entities
- Search functionality with relevance scoring
- Reporting queries with date ranges, filters, and aggregations
External Integrations
Integrate with third-party services, external authentication providers, payment processors, or notification systems.
Examples:
- Payment gateway integration for transaction processing
- Email service integration for user notifications
- SMS messaging for multi-factor authentication
- Third-party API calls for data enrichment
Computed Fields
Calculate values at query time, derive data from multiple sources, or perform runtime transformations.
Examples:
- User's full name from first and last name fields
- Order total from line items
- Age calculated from birthdate
- Formatted addresses from component fields
Custom Operations
Implement batch operations, import/export functionality, report generation, or data migration tasks.
Examples:
- Bulk user import from CSV files
- PDF report generation
- Data export to external systems
- Database migration scripts
Custom Resolver Workflow
Step 1: Define Custom Operations
You define custom operations in your schema by marking them with a special directive that tells the DDK these operations require custom implementation.
Operation Types:
- Queries — Retrieve data with custom logic (e.g., search, filtered lists, computed data)
- Mutations — Modify data with business rules (e.g., complex validation, multi-step processes)
- Subscriptions — Real-time updates over persistent connections (e.g., live notifications, data streams)
Step 2: Generate Scaffolding
When you regenerate your server, the DDK detects your custom operation definitions and generates starter files for implementing them—but only if these files don't already exist. This means:
- First generation — Creates new scaffolding files with placeholder implementations
- Subsequent generations — Preserves existing implementations, never overwriting them
- New operations — Only generates scaffolding for newly-added custom operations
The generated scaffolding includes:
- Function signatures matching your schema definitions
- Access to database connections and data repositories
- Test file templates for unit and integration testing
- Error handling structure
Step 3: Implement Business Logic
Edit the generated scaffolding files to implement your custom logic. Your implementation has access to:
Database Access:
- Active database transaction for consistency
- Typed repositories for querying and modifying entities
- Query options for filtering, sorting, and pagination
Request Context:
- User authentication information from JWT tokens
- Request-specific logging with correlation IDs
- Session and tenant information
External Services:
- Caching layer for performance optimization
- Redis client for distributed state
- Schema introspection for dynamic queries
Your implementation can:
- Query multiple database tables and combine results
- Apply complex validation rules before saving data
- Call external APIs and integrate responses
- Calculate derived values and aggregations
- Trigger asynchronous processes
Step 4: Regenerate Safely
When you add more custom operations:
- Update your schema with new operation definitions
- Regenerate the server
- Existing implementations are preserved — the generator skips files that already exist
- New scaffolding files are created only for new operations
- Generated standard resolvers are updated if the schema changed
This workflow ensures you can continuously evolve your schema and regeneration without losing custom business logic.
Capabilities
Transaction Management
All custom resolvers operate within database transactions, ensuring data consistency:
Automatic Transaction Handling:
- Each request begins a new database transaction
- All queries and mutations execute within this transaction
- Successful operations commit automatically
- Errors trigger automatic rollback to maintain consistency
- Savepoints enable partial rollback of multi-step operations
Benefits:
- Guaranteed data consistency across multiple database operations
- No partial writes if an error occurs mid-operation
- Isolation from concurrent requests
- Simplified error recovery
Repository Access
Instead of writing raw database queries, custom resolvers use generated type-safe repositories:
Available Operations:
- Retrieve all records or paginated batches
- Query by single or multiple field values
- Filter with custom conditions
- Retrieve single records by ID or field values
- Create, update, or delete records
- Batch operations for efficiency
Query Options:
- Eager-loading of relationships to avoid N+1 queries
- Sorting by one or more fields
- Transaction participation
- Debug mode for SQL query inspection
Benefits:
- Type safety prevents field name typos
- Consistent error handling across operations
- Automatic SQL injection protection
- Built-in validation of sort fields and parameters
Error Handling
The DDK provides structured error handling for custom resolvers:
Error Types:
- Database errors trigger automatic transaction rollback
- Validation errors return structured messages to clients
- Business logic errors can provide user-friendly messages
- System errors are logged with full context for debugging
Error Recovery:
- Failed operations automatically roll back to savepoints
- Structured error responses include request correlation IDs
- Errors are logged with stack traces for investigation
- Users receive appropriate error messages without system details
Context Values
Custom resolvers have access to request-specific context:
Authentication Information:
- JWT tokens from request headers
- Parsed user claims (user ID, roles, permissions)
- Session identifiers
Request Metadata:
- Correlation IDs for tracing requests across systems
- Request-specific loggers with automatic context
- Timestamp and origin information
System Resources:
- Active database transaction
- Redis client for caching
- Schema introspection capabilities
Relationship Loading
Efficiently load related data to avoid performance issues:
Eager Loading: Load related entities in a single query rather than making separate database calls for each relationship. For example, when retrieving a user, you can load all their posts and comments in one query.
Configurable Loading: Specify which relationships to load based on the request context—load minimal data for list views, comprehensive data for detail views.
Performance Optimization: Avoid N+1 query problems where a list of N items triggers N+1 database queries. Eager loading reduces this to a single query or small number of queries.
Preservation Across Regeneration
The DDK uses multiple mechanisms to preserve custom code when regenerating:
File Naming Convention
Custom resolver files use a distinct naming pattern that marks them as "do not overwrite":
- Custom files follow a specific naming convention
- Generator skips any file matching this pattern
- Only new custom operations generate new files
- Existing custom implementations are never touched
This convention creates a clear contract: if you write business logic in a custom file, regeneration will never overwrite it.
Generated File Updates
Standard generated resolver files are updated during regeneration:
- Function signatures are updated if schema changes
- Existing implementation bodies are preserved where possible
- New operations are added without affecting existing ones
Bidirectional Synchronization
Before and after regeneration, files are synchronized between working directories to ensure custom implementations are preserved and new generations are captured.
Best Practices
1. Use Transactions
All database operations in custom resolvers should use the provided database transaction:
Why:
- Ensures consistency with other operations in the request
- Enables automatic rollback on errors
- Supports savepoint-based recovery
- Maintains isolation from concurrent requests
2. Prefer Repository Methods
Use the typed repositories rather than raw database queries when possible:
Benefits:
- Type-safe field names prevent typos
- Validated sorting and filtering
- Consistent error handling
- Protection against SQL injection
- Simpler code maintenance
When to use raw queries:
- Complex joins not well-expressed through repositories
- Performance-critical queries requiring optimization
- Database-specific features like full-text search
3. Validate Input Early
Check and validate all input parameters before performing database operations:
Validation Checks:
- Required fields are present
- Values are within acceptable ranges
- Formats match expectations (emails, URLs, dates)
- Business rule constraints are satisfied
Benefits:
- Fail fast with clear error messages
- Avoid partial database writes
- Reduce transaction time
- Improve user experience with specific error messages
4. Preload Relationships
Avoid N+1 query problems by loading related data eagerly:
Problem: When retrieving a list of users and their posts, naive implementation makes one query for users, then N separate queries for each user's posts (N+1 total queries).
Solution: Specify relationship preloading to fetch users and all their posts in a single query or small number of optimized queries.
Benefits:
- Dramatically improved performance
- Reduced database load
- Faster response times
- Predictable query count
5. Document Complex Logic
Add comments explaining the "why" behind non-obvious business rules:
What to document:
- Business rules and their origins
- Edge cases and how they're handled
- External system integration details
- Performance optimization rationale
What NOT to document:
- Obvious operations (e.g., "this creates a user")
- Simple CRUD operations
- Standard patterns
6. Use Caching
Leverage the Redis client for frequently-accessed data:
Good candidates for caching:
- Configuration data that changes infrequently
- User session information
- Expensive query results
- API responses from external services
Cache Strategies:
- Set appropriate expiration times
- Implement cache invalidation on updates
- Handle cache misses gracefully
- Use consistent cache key formats
Common Patterns
Authentication Check
Retrieve and validate user identity from request context:
Use Cases:
- Operations requiring authenticated users
- Retrieving current user's data
- Personalizing responses based on user
Validation:
- Check authentication claims exist
- Extract user identifier from claims
- Verify user exists in database
- Return appropriate errors for unauthenticated requests
Authorization Check
Verify users have permission to perform operations:
Use Cases:
- Role-based access control
- Resource ownership verification
- Permission-based feature access
Patterns:
- Check user role from authentication claims
- Verify resource ownership (e.g., user can only edit their own data)
- Combine multiple authorization checks (role AND ownership)
- Return "forbidden" errors when authorization fails
Pagination
Return large result sets in manageable pages:
Benefits:
- Reduced memory usage
- Faster response times
- Better user experience
Parameters:
- Page number or offset
- Page size (results per page)
- Total count for UI pagination controls
Implementation:
- Calculate offset from page number and size
- Query with limit and offset
- Return results with pagination metadata
Aggregation
Calculate summary statistics or grouped results:
Use Cases:
- Dashboard analytics (total users, average age, count by role)
- Reporting (sales by month, orders by status)
- Metrics (active users, conversion rates)
Operations:
- Count records matching criteria
- Calculate averages, sums, minimums, maximums
- Group by specific fields
- Combine multiple aggregations
Multi-Field Filtering
Query with multiple optional filter criteria:
Implementation:
- Build filter map dynamically based on provided parameters
- Handle optional filters (only add if provided)
- Support array values for "IN" queries
- Apply additional filtering after repository query if needed
Benefits:
- Flexible search functionality
- Single endpoint for multiple filter combinations
- Type-safe filter application
Testing Approaches
Unit Testing
Test custom resolver logic with mocked dependencies:
What to Mock:
- Database repositories
- External API clients
- Cache/Redis connections
- Authentication context
What to Test:
- Business logic correctness
- Validation rules
- Error handling
- Edge cases
Benefits:
- Fast test execution
- Isolated testing of business logic
- No database dependency
- Deterministic results
Integration Testing
Test resolvers through the API with real database:
What to Test:
- End-to-end operation flow
- Database transaction behavior
- Actual query results
- API request/response format
Setup:
- Use test database or embedded database
- Seed test data
- Clean up after tests
Benefits:
- Validates complete integration
- Catches schema mismatch issues
- Tests actual database behavior
Manual Testing
Use the built-in GraphQL playground for interactive testing:
How:
- Access playground at server URL
- Write queries and mutations manually
- Inspect results and errors
- Test edge cases interactively
Benefits:
- Rapid iteration during development
- Visual feedback
- Easy sharing of test cases
- No test code required
Troubleshooting
Common Issues
Resolver Not Found: Ensure function naming matches generated patterns and schema definitions.
Type Mismatch: Verify return types match schema exactly, including nullable vs non-nullable fields.
Transaction Errors: Always retrieve the transaction from request context, don't create new transactions.
Nil Pointer Errors: Check for optional parameters (nullable types) before using them, and validate context values exist before type assertions.
Relationship Loading Failures: Verify relationship mappings in schema are correct and match database foreign keys.
Query Validation Errors: Repository methods validate field names against schema—ensure field names match exactly.
Related Documentation
- Schema Guide - How to define schemas and custom operations
- Architecture - Understanding the generated code structure
- Examples - Real-world schema and resolver examples
- FAQs - Common questions and answers
