The builder pattern provides a clean and extensible approach to constructing complex objects with multiple optional parameters, offering type safety and improved readability over traditional constructors or factory methods in backend systems.
1. Problem Statement & Requirements
Clarifying questions
- What is the scope of "Builder-based object creation system"? Is this for a specific domain (e.g., configuration, data models) or a general-purpose builder framework?
- What scale expectations are there? Are we designing for high-throughput object creation (e.g., millions/sec) or moderate usage with complex nested objects?
- What consistency requirements exist? Should the builder system support atomicity for complex object creation, or is eventual consistency acceptable?
- What's the read/write ratio? Is this primarily used for object creation, or will it also support updates and retrieval?
- What latency sensitivity is expected? Is sub-millisecond creation required, or can the system tolerate higher latencies for complex object builds?
- What authentication/authorization model is in place? Will the builder system be accessible via API, or only internally within the application?
- Are there any external integrations or dependencies? Will the system need to integrate with other services for validation or metadata?
Functional requirements
- Support creation of complex nested objects using a fluent builder API
- Allow for validation during object construction (e.g., required fields, constraints)
- Support immutable object creation with a clean API
- Enable chaining of builder methods for intuitive usage
- Allow for conditional object building based on configuration or context
- Support inheritance and composition of builders
- Provide support for builder state management (e.g., tracking which fields have been set)
- Enable object creation from JSON or other serialized formats
- Support for builder reusability and cloning
Non-functional requirements
- Latency targets: p50 < 1ms, p95 < 5ms, p99 < 10ms for typical object creation
- Throughput: 50,000+ object creations/sec under load (100 concurrent threads)
- Availability: 99.9% uptime with graceful degradation capability
- Scalability: Support horizontal scaling across multiple application instances
- Memory usage: Efficient object creation with minimal memory overhead
Constraints and assumptions
- Assumption: The system will be used within a single application or bounded context
- Constraint: Builder patterns should not introduce significant performance overhead
- Assumption: All builder operations will be synchronous (no async builders)
- Constraint: Builder state should not leak sensitive information or cause security issues
- Assumption: Object creation will be primarily done in a single-threaded context (e.g., request handling)
- Constraint: Builder implementations should be compatible with existing object models and validation logic
Out of scope
- Builder system integration with external services or third-party tools (e.g., UI form builders, IDE plugins)
- Builder versioning or migration support for legacy builder configurations
- Support for real-time collaborative object building (e.g., multiple users editing the same object simultaneously)
- Builder UI or web-based editor interfaces
- Builder persistence or storage (builders themselves are not persisted, only the built objects)
- Cross-language builder support (e.g., Python or JavaScript builders for JVM-based systems)
2. API Design
API Style Selection
REST is selected for its simplicity, wide adoption, and clear HTTP semantics. It provides well-defined endpoints with standard methods (GET, POST, PUT, DELETE) that align with the CRUD operations needed for object creation and management. REST also supports content negotiation, caching, and is easily consumable by various clients.
Endpoint Definitions
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/builder | Create a new builder configuration | JWT |
| GET | /v1/builder/{id} | Retrieve a builder by ID | JWT |
| PUT | /v1/builder/{id} | Update an existing builder configuration | JWT |
| DELETE | /v1/builder/{id} | Delete a builder configuration | JWT |
| POST | /v1/builder/{id}/build | Execute build using the builder config | JWT |
| GET | /v1/builder/{id}/builds | List all builds for a builder | JWT |
| GET | /v1/builder/{id}/builds/{buildId} | Retrieve a specific build result | JWT |
Request and Response Schemas
Create Builder Request
{
"name": "MyBuilder",
"description": "A sample builder for demonstration",
"config": {
"source": "github.com/myorg/myrepo",
"branch": "main",
"buildScript": "./build.sh",
"environment": {
"JAVA_HOME": "/usr/lib/jvm/java-11-openjdk",
"NODE_VERSION": "16"
}
},
"triggers": {
"onPush": true,
"onPullRequest": false,
"schedule": "0 0 * * *"
}
}
Create Builder Response
{
"id": "b5a3f1c9-8d4e-4b2a-9f7e-3d1a6c5b8e9f",
"name": "MyBuilder",
"description": "A sample builder for demonstration",
"config": {
"source": "github.com/myorg/myrepo",
"branch": "main",
"buildScript": "./build.sh",
"environment": {
"JAVA_HOME": "/usr/lib/jvm/java-11-openjdk",
"NODE_VERSION": "16"
}
},
"triggers": {
"onPush": true,
"onPullRequest": false,
"schedule": "0 0 * * *"
},
"createdAt": "2023-10-01T12:00:00Z",
"updatedAt": "2023-10-01T12:00:00Z"
}
Build Execution Request
{
"buildId": "b5a3f1c9-8d4e-4b2a-9f7e-3d1a6c5b8e9f",
"parameters": {
"buildNumber": "12345",
"commitHash": "abc123def456"
}
}
Build Execution Response
{
"buildId": "b5a3f1c9-8d4e-4b2a-9f7e-3d1a6c5b8e9f",
"status": "running",
"createdAt": "2023-10-01T12:00:00Z",
"updatedAt": "2023-10-01T12:00:00Z"
}
HTTP Status Codes and Error Response Format
| Status Code | Description |
|---|---|
| 200 | Success (GET, PUT) |
| 201 | Created (POST) |
| 400 | Bad Request (malformed input, validation errors) |
| 401 | Unauthorized (missing or invalid auth) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found (resource not found) |
| 409 | Conflict (duplicate resource) |
| 422 | Unprocessable Entity (validation errors) |
| 429 | Too Many Requests (rate limiting) |
| 500 | Internal Server Error |
Standardized error response format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input provided",
"details": [
{
"field": "name",
"message": "Name is required"
}
],
"timestamp": "2023-10-01T12:00:00Z"
}
}
API Versioning Strategy
Versioning is done via path prefix (/v1/) to maintain backward compatibility and allow for future evolution. This approach is simple, explicit, and follows REST conventions.
Pagination Design
Cursor-based pagination is used for /builds endpoint to handle large datasets efficiently and prevent issues with data changes during pagination. It provides consistent results even when items are added or removed between pages.
Authentication and Authorization
All endpoints require JWT-based authentication. Authorization is handled through RBAC where users must have appropriate roles (e.g., builder_admin, builder_user) to perform actions on builders.
Rate Limiting
Rate limiting is implemented per endpoint:
POST /v1/builder: 100 requests/minuteGET /v1/builder/{id}: 500 requests/minutePOST /v1/builder/{id}/build: 50 requests/minute
Rate limiting is enforced using a sliding window algorithm with Redis-based token buckets.
Idempotency Design
For mutation endpoints (POST, PUT, DELETE), idempotency keys are supported via the Idempotency-Key header. The system stores a record of the operation and returns the same result for duplicate requests within a time window (e.g., 5 minutes).
Example Idempotency Flow
POST /v1/builder HTTP/1.1
Idempotency-Key: 3a9f4c7b-6d2e-4a1f-b5c8-9d3e7f2a1b4c
Content-Type: application/json
{
"name": "MyBuilder",
"config": { ... }
}
If the same key is used within the window, a 200 OK response with the existing resource is returned.
3. Data Model Design
Entity-Relationship diagram (Mermaid)
erDiagram
BUILD_CONFIG {
string id PK
string name
string description
jsonb config
timestamp created_at
timestamp updated_at
string created_by
string updated_by
}
BUILD_TEMPLATE {
string id PK
string name
string description
jsonb template_content
timestamp created_at
timestamp updated_at
string created_by
string updated_by
}
BUILD_REQUEST {
string id PK
string build_config_id FK
string template_id FK
jsonb parameters
string status
timestamp created_at
timestamp updated_at
string requested_by
string build_id
}
BUILD {
string id PK
string build_request_id FK
string status
timestamp started_at
timestamp completed_at
jsonb output
string error_message
timestamp created_at
timestamp updated_at
}
BUILD_CONFIG ||--o{ BUILD_REQUEST : "has"
BUILD_TEMPLATE ||--o{ BUILD_REQUEST : "uses"
BUILD_REQUEST ||--o{ BUILD : "triggers"
Table / Collection schema definitions with field types, constraints, indices
-- Build Configuration table
CREATE TABLE build_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
config JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by VARCHAR(255) NOT NULL,
updated_by VARCHAR(255) NOT NULL,
CONSTRAINT chk_config_not_empty CHECK (jsonb_typeof(config) = 'object')
);
-- Build Template table
CREATE TABLE build_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
template_content JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by VARCHAR(255) NOT NULL,
updated_by VARCHAR(255) NOT NULL,
CONSTRAINT chk_template_content_not_empty CHECK (jsonb_typeof(template_content) = 'object')
);
-- Build Request table
CREATE TABLE build_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
build_config_id UUID NOT NULL REFERENCES build_configs(id) ON DELETE CASCADE,
template_id UUID NOT NULL REFERENCES build_templates(id) ON DELETE CASCADE,
parameters JSONB NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
requested_by VARCHAR(255) NOT NULL,
build_id VARCHAR(255),
CONSTRAINT chk_parameters_not_empty CHECK (jsonb_typeof(parameters) = 'object')
);
-- Build table
CREATE TABLE builds (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
build_request_id UUID NOT NULL REFERENCES build_requests(id) ON DELETE CASCADE,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
output JSONB,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_build_configs_created_by ON build_configs(created_by);
CREATE INDEX idx_build_configs_updated_at ON build_configs(updated_at);
CREATE INDEX idx_build_templates_created_by ON build_templates(created_by);
CREATE INDEX idx_build_requests_build_config_id ON build_requests(build_config_id);
CREATE INDEX idx_build_requests_template_id ON build_requests(template_id);
CREATE INDEX idx_build_requests_status ON build_requests(status);
CREATE INDEX idx_build_requests_requested_by ON build_requests(requested_by);
CREATE INDEX idx_builds_build_request_id ON builds(build_request_id);
CREATE INDEX idx_builds_status ON builds(status);
CREATE INDEX idx_builds_started_at ON builds(started_at);
Primary key strategy (UUID vs. auto-increment vs. ULID — with justification)
Using UUID as primary keys for all tables. Justification:
-
Distributed system compatibility: UUIDs are globally unique, making them ideal for distributed systems where multiple services or instances may create entities concurrently.
-
No coordination required: Unlike auto-increment, UUIDs don't require coordination between services or databases to ensure uniqueness.
-
Scalability: UUIDs support horizontal scaling without requiring database clustering or complex ID generation logic.
-
Security: UUIDs don't expose sequential patterns that could be exploited for enumeration attacks.
-
Database independence: UUIDs work consistently across different database systems without requiring specific ID generation features.
Index design (composite, partial, covering indices — with justification per index)
| Index Name | Columns | Type | Purpose |
|---|---|---|---|
| idx_build_configs_created_by | created_by | Standard | Fast lookup by creator |
| idx_build_configs_updated_at | updated_at | Standard | Efficient time-based queries |
| idx_build_templates_created_by | created_by | Standard | Fast lookup by creator |
| idx_build_requests_build_config_id | build_config_id | Standard | Efficient filtering by config |
| idx_build_requests_template_id | template_id | Standard | Efficient filtering by template |
| idx_build_requests_status | status | Standard | Fast query by build status |
| idx_build_requests_requested_by | requested_by | Standard | Fast lookup by requester |
| idx_builds_build_request_id | build_request_id | Standard | Fast lookup by associated request |
| idx_builds_status | status | Standard | Efficient filtering by build status |
| idx_builds_started_at | started_at | Standard | Time-based queries for active builds |
Data partitioning / sharding strategy
Partitioning by created_at timestamp with monthly granularity:
-- Example of partitioned build_requests table by month
CREATE TABLE build_requests_2024_01 (
CHECK (created_at >= '2024-01-01' AND created_at < '2024-02-01')
) INHERITS (build_requests);
CREATE TABLE build_requests_2024_02 (
CHECK (created_at >= '2024-02-01' AND created_at < '2024-03-01')
) INHERITS (build_requests);
Justification:
- Time-based access patterns: Most queries will filter by creation date for audit and reporting.
- Data lifecycle management: Easy to archive or drop old partitions.
- Query performance: Reduces scan size for time-range queries.
Soft delete vs. hard delete decision with justification
Soft delete is implemented using a deleted_at timestamp field in the build_configs and build_templates tables.
-- Extended schema with soft delete fields
ALTER TABLE build_configs ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE build_templates ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
-- Index for soft-deleted records
CREATE INDEX idx_build_configs_deleted_at ON build_configs(deleted_at);
CREATE INDEX idx_build_templates_deleted_at ON build_templates(deleted_at);
Justification:
- Auditability: Historical data is preserved for compliance and debugging.
- Recovery capability: Data can be restored if accidentally deleted.
- Referential integrity: Prevents cascading deletes that could break dependent systems.
Audit trail design (who, what, when)
All tables include created_by, updated_by fields and created_at, updated_at timestamps.
-- Audit trail view for build_configs
CREATE VIEW build_configs_audit AS
SELECT
id,
name,
description,
config,
created_at,
updated_at,
created_by,
updated_by,
CASE WHEN deleted_at IS NOT NULL THEN 'DELETED' ELSE 'ACTIVE' END AS status
FROM build_configs;
Data archival strategy and retention policy
- Archival: Build requests older than 3 years are archived to cold storage (S3 or similar).
- Retention:
- Build configs and templates: 5 years (compliance requirement)
- Build requests and builds: 2 years (audit requirement)
- Archival process:
- Move to S3 with metadata stored in database
- Update
archivedboolean flag on records - Maintain reference to archived data location
-- Archival status column
ALTER TABLE build_requests ADD COLUMN archived BOOLEAN DEFAULT FALSE;
ALTER TABLE builds ADD COLUMN archived BOOLEAN DEFAULT FALSE;
4. Class Design & Service Layer
classDiagram
class BuilderSystem {
+createObject()
+validateObject()
+build()
}
class ObjectBuilder {
-object: T
-validators: List~Validator~
+withField()
+build()
+validate()
}
class ConcreteBuilder {
-product: Product
+withName()
+withDescription()
+build()
}
class Product {
-id: String
-name: String
-description: String
-createdAt: LocalDateTime
}
class Validator {
+validate()
}
class ValidationRule {
-field: String
-rule: String
+validate()
}
class ObjectFactory {
-builderMap: Map~String, Class~ObjectBuilder~
+createBuilder()
+buildObject()
}
BuilderSystem --> ObjectBuilder : uses
ObjectBuilder <|-- ConcreteBuilder : implements
ConcreteBuilder --> Product : creates
ObjectBuilder --> Validator : validates with
Validator <|-- ValidationRule : implements
BuilderSystem --> ObjectFactory : delegates to
ObjectFactory --> ObjectBuilder : creates
stateDiagram-v2
[*] --> Idle
Idle --> Building : startBuild()
Building --> Validating : validate()
Validating --> Ready : success()
Validating --> Idle : failure()
Ready --> Built : build()
Built --> [*] : finalize()
Service Class Structure and Responsibilities
The Builder System follows a layered architecture with clear separation of concerns:
- BuilderSystem - Main orchestrator that manages the entire object creation lifecycle
- ObjectBuilder - Abstract base class for concrete builders with shared functionality
- ConcreteBuilder - Type-specific builder implementations that construct domain objects
- ObjectFactory - Factory pattern implementation for creating builders by type
- Validator - Validation framework that ensures object integrity
Interface / Abstract Class Design
// Builder interface for type-safe object construction
public interface ObjectBuilder<T> {
T withField(String field, Object value);
T build();
boolean validate();
}
// Abstract base builder with common functionality
public abstract class AbstractObjectBuilder<T> implements ObjectBuilder<T> {
protected T object;
protected List<Validator> validators = new ArrayList<>();
public AbstractObjectBuilder<T> withField(String field, Object value) {
// Common validation and field setting logic
return (AbstractObjectBuilder<T>) this;
}
public AbstractObjectBuilder<T> addValidator(Validator validator) {
validators.add(validator);
return this;
}
public boolean validate() {
return validators.stream().allMatch(Validator::validate);
}
}
// Factory interface for builder creation
public interface BuilderFactory {
ObjectBuilder<?> createBuilder(String type);
}
Business Logic Encapsulation Patterns
The system implements the Builder Pattern with a Factory pattern for type-safe object creation. The Strategy pattern is used for validation rules:
// Strategy pattern for validation rules
public interface Validator {
boolean validate(Object object);
}
// Concrete validator implementations
public class RequiredFieldValidator implements Validator {
private final String fieldName;
public RequiredFieldValidator(String fieldName) {
this.fieldName = fieldName;
}
@Override
public boolean validate(Object object) {
// Implementation for required field validation
return true;
}
}
// Factory pattern for creating builders
public class BuilderFactoryImpl implements BuilderFactory {
private final Map<String, Class<? extends ObjectBuilder>> builderMap = new HashMap<>();
public BuilderFactoryImpl() {
builderMap.put("product", ProductBuilder.class);
builderMap.put("user", UserBuilder.class);
}
@Override
public ObjectBuilder<?> createBuilder(String type) {
Class<? extends ObjectBuilder> builderClass = builderMap.get(type);
if (builderClass == null) {
throw new IllegalArgumentException("Unknown builder type: " + type);
}
try {
return builderClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to create builder", e);
}
}
}
Critical-Path Method Implementation
public class BuilderSystem {
private final ObjectFactory objectFactory;
private final Validator validator;
public BuilderSystem(ObjectFactory factory, Validator validator) {
this.objectFactory = factory;
this.validator = validator;
}
public <T> T createObject(String type, String builderType,
Map<String, Object> fields) throws ValidationException {
// Transaction boundary - all operations happen in single transaction
return executeInTransaction(() -> {
try {
// 1. Create appropriate builder for type
ObjectBuilder<T> builder = (ObjectBuilder<T>) objectFactory.createBuilder(builderType);
// 2. Apply fields using the builder
T object = applyFieldsToBuilder(builder, fields);
// 3. Validate the constructed object
if (!builder.validate()) {
throw new ValidationException("Object validation failed");
}
// 4. Publish object creation event (async)
publishObjectCreatedEvent(object, type);
return object;
} catch (Exception e) {
// Rollback transaction if needed
throw new RuntimeException("Failed to create object", e);
}
});
}
private <T> T applyFieldsToBuilder(ObjectBuilder<T> builder,
Map<String, Object> fields) {
for (Map.Entry<String, Object> field : fields.entrySet()) {
builder.withField(field.getKey(), field.getValue());
}
return builder.build();
}
private void publishObjectCreatedEvent(Object object, String type) {
// Publish to async event system (e.g., Kafka)
// This would be implemented with proper async handling and retries
}
private <T> T executeInTransaction(Supplier<T> operation) {
// Transaction management logic here
// In real implementation, would use Spring's @Transactional or similar
return operation.get();
}
}
Transaction Boundary Design
The transaction boundary encompasses:
- Builder instantiation and configuration
- Object validation using all registered validators
- Object persistence (if applicable)
- Event publishing to external systems
Service Interface Contracts
public interface BuilderService {
<T> T createObject(String type, String builderType, Map<String, Object> fields)
throws ValidationException;
<T> T validateObject(T object, String validatorType);
boolean isBuilderSupported(String type);
}
Saga / Orchestration Pattern
For distributed object creation involving external systems:
- Each builder operation is atomic within a local transaction
- External system interactions use the Outbox pattern for reliability
- Compensation logic is implemented via event-driven workflows
Domain Object / Value Object Design
public class Product {
private final String id;
private final String name;
private final String description;
private final LocalDateTime createdAt;
public Product(String id, String name, String description, LocalDateTime createdAt) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.description = description;
this.createdAt = Objects.requireNonNull(createdAt);
}
// Getters only - immutable object
public String getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
5. Concurrency & Thread Safety
Concurrency Model
The builder-based object creation system will operate in a multi-threaded environment where multiple concurrent clients may request object construction simultaneously. The system uses a thread-safe approach with:
- Thread-local storage for temporary builder state (when applicable)
- Immutable builder instances to prevent race conditions
- Synchronized access to shared resources (e.g., object pools, configuration caches)
- Lock-free data structures where possible (e.g., concurrent hash maps for caching)
Race Condition Prevention Strategy
Race conditions can occur in the following scenarios:
- Builder pool contention: Multiple threads trying to acquire builders from a shared pool
- Shared configuration updates: Concurrent modification of global builder configurations
- Object state consistency: Builder instances modifying shared state during construction
Prevention mechanisms:
- Use atomic reference updates for builder pools
- Implement read-write locks for configuration data structures
- Ensure immutable intermediate state in builder instances
- Apply thread-local caching for per-thread temporary data
Optimistic vs. Pessimistic Locking Selection
For the builder system, optimistic locking is preferred over pessimistic locking because:
- Builder operations are typically fast and atomic
- Concurrent object creation is the primary use case, not shared resource contention
- Builder instances are immutable once built, reducing conflict probability
- Pessimistic locking would introduce unnecessary overhead and reduce throughput
Deadlock Prevention Strategy
Deadlock prevention is handled through:
- Lock ordering: All builder pool operations follow a consistent acquisition order
- Timeout-based locks: Use
tryLock()with timeouts to prevent indefinite blocking - No nested locking: Builder instances do not hold multiple locks simultaneously
- Thread-safe object pool: Implement a bounded thread-safe queue for builder reuse
Idempotency Implementation for Concurrent Duplicate Requests
For concurrent duplicate requests, the system implements:
- Idempotency keys: Clients provide unique identifiers for each request
- In-memory cache: Track recent idempotency keys with TTL to prevent duplicate processing
- Atomic operation: Use atomic operations to check and store idempotency keys
public class IdempotentBuilderService {
private final Map<String, CompletableFuture<BuildResult>> idempotencyCache =
new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public CompletableFuture<BuildResult> buildWithIdempotency(
String idempotencyKey,
BuilderConfig config) {
return idempotencyCache.computeIfAbsent(idempotencyKey, key -> {
CompletableFuture<BuildResult> future = new CompletableFuture<>();
// Schedule cleanup of cache entry after TTL
scheduler.schedule(() -> idempotencyCache.remove(key), 5, TimeUnit.MINUTES);
// Process the build operation
try {
BuildResult result = processBuild(config);
future.complete(result);
} catch (Exception e) {
future.completeExceptionally(e);
}
return future;
});
}
}
The system ensures that concurrent requests with the same idempotency key return the same result without reprocessing, while maintaining thread safety and preventing resource contention.
6. Sequence Diagrams for Key Flows
Happy Path Sequence Diagram
sequenceDiagram
participant Client
participant BuilderService
participant BuilderFactory
participant Builder
participant ValidationService
participant EntityRepository
Client->>BuilderFactory: createBuilder(type)
BuilderFactory->>Builder: new Builder(type)
Builder->>Client: return Builder instance
Client->>Builder: withField1(value1)
Builder->>Builder: setField1(value1)
Client->>Builder: withField2(value2)
Builder->>Builder: setField2(value2)
Client->>Builder: build()
Builder->>ValidationService: validate()
ValidationService-->>Builder: validation result
Builder->>EntityRepository: save(entity)
EntityRepository-->>Builder: entity ID
Builder->>Client: return built entity
Error / Edge Case Sequence Diagram
sequenceDiagram
participant Client
participant BuilderService
participant BuilderFactory
participant Builder
participant ValidationService
Client->>BuilderFactory: createBuilder(type)
BuilderFactory->>Builder: new Builder(type)
Builder->>Client: return Builder instance
Client->>Builder: withField1(null)
Builder->>Builder: setField1(null)
Client->>Builder: build()
Builder->>ValidationService: validate()
ValidationService-->>Builder: validation error
Builder->>Client: throw ValidationException
Async Flow Sequence Diagram
sequenceDiagram
participant Client
participant BuilderService
participant EventPublisher
participant AsyncEventQueue
participant BuilderProcessor
Client->>BuilderService: submitBuildRequest()
BuilderService->>EventPublisher: publishBuildEvent(event)
EventPublisher->>AsyncEventQueue: queue event
AsyncEventQueue-->>BuilderProcessor: consume event
BuilderProcessor->>BuilderService: processBuildEvent(event)
BuilderService->>BuilderFactory: createBuilder(type)
BuilderService->>EntityRepository: save(entity)
BuilderService->>EventPublisher: publishSuccessEvent()
EventPublisher->>Client: notify success
7. Caching Design
Cache Strategy Selection
For the builder-based object creation system, we'll use a cache-aside pattern with write-through behavior for critical data. This approach provides the best balance between consistency and performance for a system where object creation is the primary operation.
The cache-aside pattern allows us to:
- Keep control over when data is loaded into cache
- Handle cache misses gracefully with fallbacks to database
- Implement proper cache invalidation strategies
Cache Key Design
Cache keys will follow a structured namespace pattern:
builder:{builderId}:{objectType}:{objectId}
builder:config:{builderId}
builder:template:{templateId}
TTL Strategy
| Data Type | TTL | Reason |
|---|---|---|
| Builder configurations | 1 hour | Configuration changes are infrequent but should not be stale for too long |
| Object templates | 24 hours | Templates are relatively static and expensive to compute |
| Built objects (recently created) | 5 minutes | High read frequency for recently built objects |
| Builder metadata | 1 hour | Metadata changes less frequently than actual builds |
Cache Eviction Policy
We'll use LRU (Least Recently Used) eviction policy with a secondary TTL-based cleanup. This ensures:
- Frequently accessed builders remain in cache
- Memory pressure is managed through time-based expiration
- Cache warming can occur for hot builders
Cache Stampede Prevention
To prevent cache stampedes:
- Probabilistic early expiry: Set TTL to 75% of configured value and use random jitter (±10%) for cache entries
- Distributed locks: Use Redis distributed locks when a cache miss occurs to prevent multiple concurrent rebuilds
- Cache warming: Pre-warm hot builders during low-traffic periods
Distributed Cache Architecture
We'll use Redis Cluster with:
- 3 master nodes for data distribution
- 3 replica nodes per master for high availability
- Redis Sentinel for automatic failover
- Cache sharding by builder ID using consistent hashing
Cache Invalidation Strategy
Cache invalidation will be event-driven:
- When a builder configuration changes → invalidate
builder:{builderId}:*keys - When an object template is updated → invalidate
builder:template:{templateId}key - When a builder completes an object build → invalidate
builder:{builderId}:{objectType}:{objectId}key
Cache Implementation Example (Java)
@Component
public class BuilderCache {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
public BuilderConfig getBuilderConfig(String builderId) {
String cacheKey = "builder:config:" + builderId;
// Try cache first
BuilderConfig cached = (BuilderConfig) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// Cache miss - acquire lock to prevent stampede
String lockKey = "lock:builder:" + builderId;
if (acquireDistributedLock(lockKey, 5000)) {
try {
// Double-check cache after lock acquisition
cached = (BuilderConfig) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// Load from database
BuilderConfig config = builderRepository.findById(builderId);
if (config != null) {
// Cache with TTL and random jitter
long ttl = 3600 + ThreadLocalRandom.current().nextInt(0, 720);
redisTemplate.opsForValue().set(cacheKey, config, ttl, TimeUnit.SECONDS);
}
return config;
} finally {
releaseDistributedLock(lockKey);
}
}
// Fallback to database if lock acquisition fails
return builderRepository.findById(builderId);
}
public void invalidateBuilderCache(String builderId) {
String pattern = "builder:" + builderId + ":*";
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
private boolean acquireDistributedLock(String lockKey, int timeoutMs) {
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, timeoutMs, TimeUnit.MILLISECONDS);
return acquired != null && acquired;
}
private void releaseDistributedLock(String lockKey) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
UUID.randomUUID().toString()
);
}
}
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain objects and persistence concerns. Each entity will have its own repository interface and implementation.
public interface UserRepository {
User save(User user);
Optional<User> findById(UUID id);
List<User> findAll();
void delete(UUID id);
boolean existsById(UUID id);
}
public class UserRepositoryImpl implements UserRepository {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public UserRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Override
public User save(User user) {
String sql = "INSERT INTO users (id, name, email, metadata, created_at) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, user.getId(), user.getName(), user.getEmail(),
objectMapper.writeValueAsString(user.getMetadata()),
user.getCreatedAt());
return user;
}
@Override
public Optional<User> findById(UUID id) {
String sql = "SELECT * FROM users WHERE id = ?";
try {
return Optional.of(jdbcTemplate.queryForObject(sql, new UserRowMapper(), id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<User> findAll() {
return jdbcTemplate.query("SELECT * FROM users", new UserRowMapper());
}
@Override
public void delete(UUID id) {
jdbcTemplate.update("DELETE FROM users WHERE id = ?", id);
}
@Override
public boolean existsById(UUID id) {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users WHERE id = ?", Integer.class, id) > 0;
}
}
ORM vs Raw Query Decisions
For this system, raw SQL queries are preferred over ORM for the following reasons:
- Performance: Raw queries provide better performance for complex operations and reduce object mapping overhead
- Control: Direct control over SQL allows for optimization of complex queries and better handling of edge cases
- Maintainability: Explicit queries make it easier to understand and debug database interactions
The system uses JdbcTemplate for raw SQL execution with RowMapper for object mapping, providing a balance between performance and maintainability.
Read vs Write Separation
The system implements read-write separation using a CQRS (Command Query Responsibility Segregation) pattern for high-performance scenarios:
public interface UserReadRepository {
User findById(UUID id);
List<User> findAll();
long count();
}
public interface UserWriteRepository {
User save(User user);
void delete(UUID id);
void update(User user);
}
public class UserReadRepositoryImpl implements UserReadRepository {
private final JdbcTemplate jdbcTemplate;
@Override
public User findById(UUID id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ? AND deleted_at IS NULL",
new UserRowMapper(),
id);
}
@Override
public List<User> findAll() {
return jdbcTemplate.query(
"SELECT * FROM users WHERE deleted_at IS NULL",
new UserRowMapper());
}
@Override
public long count() {
return jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM users WHERE deleted_at IS NULL",
Long.class);
}
}
public class UserWriteRepositoryImpl implements UserWriteRepository {
private final JdbcTemplate jdbcTemplate;
@Override
public User save(User user) {
String sql = "INSERT INTO users (id, name, email, metadata, created_at) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, user.getId(), user.getName(), user.getEmail(),
objectMapper.writeValueAsString(user.getMetadata()),
user.getCreatedAt());
return user;
}
@Override
public void delete(UUID id) {
jdbcTemplate.update("UPDATE users SET deleted_at = NOW() WHERE id = ?", id);
}
@Override
public void update(User user) {
String sql = "UPDATE users SET name = ?, email = ?, metadata = ? WHERE id = ?";
jdbcTemplate.update(sql, user.getName(), user.getEmail(),
objectMapper.writeValueAsString(user.getMetadata()),
user.getId());
}
}
Connection Pool Configuration
Connection pool is configured with:
- Max pool size: 20 connections
- Min idle connections: 5
- Connection timeout: 30 seconds
- Validation query: "SELECT 1"
- Idle timeout: 600 seconds
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/myapp");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setValidationTimeout(5000);
config.setLeakDetectionThreshold(60000);
config.setConnectionTestQuery("SELECT 1");
return new HikariDataSource(config);
}
}
N+1 Query Prevention Strategy
To prevent N+1 query problems, the system uses eager loading with batch operations:
public class UserBatchRepository {
private final JdbcTemplate jdbcTemplate;
public List<User> findByIds(List<UUID> ids) {
if (ids.isEmpty()) return Collections.emptyList();
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT * FROM users WHERE id IN (" + placeholders + ")";
return jdbcTemplate.query(sql, ids.toArray(), new UserRowMapper());
}
public List<User> findByIdsWithMetadata(List<UUID> ids) {
// Batch fetch users with metadata in single query
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = """
SELECT u.*, m.metadata_json
FROM users u
LEFT JOIN user_metadata m ON u.id = m.user_id
WHERE u.id IN (%s)
""".formatted(placeholders);
return jdbcTemplate.query(sql, ids.toArray(), new UserWithMetadataRowMapper());
}
}
Batch Operation Design
Batch operations are implemented to reduce database round trips:
public class UserBatchOperation {
private final JdbcTemplate jdbcTemplate;
public void batchSave(List<User> users) {
String sql = "INSERT INTO users (id, name, email, metadata, created_at) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setObject(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getEmail());
ps.setString(4, objectMapper.writeValueAsString(user.getMetadata()));
ps.setTimestamp(5, Timestamp.valueOf(user.getCreatedAt()));
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
}
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature — what triggers async vs. what stays synchronous, and why
The builder system is primarily a synchronous operation that creates objects in memory or persists them to storage. However, we can introduce asynchronous processing for:
- Post-creation notifications - When a complex object is built, notify downstream systems (e.g., logging service, analytics engine, or external integrations)
- Validation and enrichment - Trigger async validation workflows that may involve external APIs or complex business logic
- Audit trail generation - Log object creation events asynchronously to avoid blocking the main operation
- Resource cleanup or archival - Schedule cleanup of temporary resources or move completed builds to archival storage
The synchronous flow handles the core object creation, while asynchronous flows handle cross-cutting concerns that don't block the user's immediate request.
Message schema design with a sample payload (Avro / Protobuf / JSON Schema — with versioning strategy)
{
"type": "object",
"title": "BuilderObjectCreatedEvent",
"version": "1.0",
"properties": {
"eventId": {
"type": "string",
"description": "Unique identifier for this event"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "When the event occurred"
},
"builderId": {
"type": "string",
"description": "Unique identifier of the builder instance"
},
"objectType": {
"type": "string",
"description": "Type of object created (e.g., 'User', 'Order')"
},
"objectId": {
"type": "string",
"description": "Unique identifier of the built object"
},
"payload": {
"type": "object",
"description": "Serialized built object data"
},
"metadata": {
"type": "object",
"description": "Additional context about the build operation"
}
},
"required": ["eventId", "timestamp", "builderId", "objectType", "objectId"]
}
Versioning strategy uses semantic versioning with backward compatibility in mind. Schema evolution is handled through schema registry (e.g., Confluent Schema Registry) with strict compatibility rules.
Dead letter queue strategy and poison message handling
For failed async processing, we implement a dead-letter queue (DLQ) strategy:
- Retry logic: Messages are retried up to 3 times with exponential backoff (500ms, 1s, 2s)
- DLQ routing: Messages that fail after max retries are sent to a dedicated DLQ topic
- Manual intervention: Failed messages in DLQ can be manually inspected and reprocessed
- Monitoring: Alert on DLQ message count exceeding threshold
Poison message handling includes:
- Message size limits (max 1MB)
- Timeout on processing (5 seconds per message)
- Circuit breaker for downstream services
At-least-once vs. exactly-once delivery — decision with justification for this feature
We choose at-least-once delivery for the builder system because:
- Idempotency is built into the system: Builder operations are idempotent when using unique identifiers
- Eventual consistency is acceptable: The system can tolerate duplicate events as long as the end state is correct
- Simpler implementation: Exactly-once requires distributed transaction patterns or complex state tracking
- Performance trade-off: At-least-once is more performant and scalable
For object creation events, duplicates are handled gracefully by checking if the object already exists.
Retry policy for consumers (max attempts, backoff formula, non-retryable error classification)
Retry policy configuration:
| Parameter | Value |
|---|---|
| Max attempts | 3 |
| Initial backoff | 500ms |
| Backoff multiplier | 2 |
| Max backoff | 10s |
| Jitter | ±25% |
Retry formula: delay = baseDelay * (2^attempt) + random_jitter
Non-retryable errors:
- Invalid object type
- Malformed payload
- Authorization failures
- Resource not found (when retrying would not help)
Outbox pattern for reliable event publishing — include or explicitly justify omission
We do not use the outbox pattern in this design for the following reasons:
- Simplicity: The builder system is a single-service application with minimal cross-service dependencies
- Eventual consistency: The system can tolerate some event loss or duplication in the short term
- Performance: Outbox pattern adds complexity and potential performance overhead for simple use cases
- Alternative approach: Using a reliable message broker (e.g., Kafka with proper acks) provides sufficient durability
Instead, we rely on:
- Message broker acknowledgments
- Retry policies with exponential backoff
- Dead letter queue handling
Sequence diagram for all async flows (Mermaid)
sequenceDiagram
participant Client
participant BuilderService
participant EventPublisher
participant MessageBroker
participant ConsumerService
Client->>BuilderService: CreateObjectRequest
BuilderService->>BuilderService: BuildObject()
BuilderService->>EventPublisher: PublishObjectCreatedEvent()
EventPublisher->>MessageBroker: Send to topic
MessageBroker->>ConsumerService: Deliver event
ConsumerService->>ConsumerService: ProcessEvent()
ConsumerService->>ConsumerService: Log or trigger follow-up actions
10. Error Handling Design
Error Classification Hierarchy
The system implements a hierarchical error classification to enable proper handling and propagation:
// Base exception hierarchy
public abstract class BaseException extends RuntimeException {
private final String errorCode;
private final int httpStatus;
public BaseException(String errorCode, int httpStatus, String message) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
}
// Getters
}
public class ValidationException extends BaseException {
public ValidationException(String message) {
super("VALIDATION_ERROR", 400, message);
}
}
public class ResourceNotFoundException extends BaseException {
public ResourceNotFoundException(String message) {
super("RESOURCE_NOT_FOUND", 404, message);
}
}
public class ConflictException extends BaseException {
public ConflictException(String message) {
super("CONFLICT_ERROR", 409, message);
}
}
public class InternalServerException extends BaseException {
public InternalServerException(String message) {
super("INTERNAL_ERROR", 500, message);
}
}
Error Propagation Strategy
Errors are handled at the appropriate layer:
- Domain Layer: Domain-specific exceptions (ValidationException, ConflictException)
- Service Layer: Business logic errors wrapped in appropriate domain exceptions
- Controller Layer: Infrastructure and system errors converted to standardized responses
Retry Strategy with Exponential Backoff
For transient infrastructure issues, the system implements a retry mechanism:
public class RetryableOperation {
private static final int MAX_RETRIES = 3;
private static final long INITIAL_DELAY_MS = 1000;
private static final double BACKOFF_MULTIPLIER = 2.0;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (attempts < MAX_RETRIES) {
try {
return operation.get();
} catch (Exception e) {
attempts++;
if (attempts >= MAX_RETRIES || !isRetryable(e)) {
throw new InternalServerException("Operation failed after " + MAX_RETRIES + " attempts", e);
}
long delay = (long) (INITIAL_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempts - 1));
// Add jitter: ±25% of delay
long jitter = (long) (delay * 0.25 * (Math.random() - 0.5) * 2);
delay += jitter;
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new InternalServerException("Retry interrupted", ie);
}
}
}
throw new InternalServerException("Operation failed after " + MAX_RETRIES + " attempts");
}
private boolean isRetryable(Exception e) {
return e instanceof TimeoutException ||
e instanceof IOException ||
(e instanceof InternalServerException &&
!e.getMessage().contains("permanent failure"));
}
}
Circuit Breaker Design
For external service dependencies, a circuit breaker pattern prevents cascading failures:
public class CircuitBreaker {
private final int failureThreshold;
private final long timeoutMs;
private final int successThreshold;
private volatile CircuitState state = CircuitState.CLOSED;
private volatile int failureCount = 0;
private volatile long lastFailureTime = 0;
public enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
public <T> T execute(Supplier<T> operation) throws Exception {
switch (state) {
case CLOSED:
return executeClosed(operation);
case OPEN:
return executeOpen(operation);
case HALF_OPEN:
return executeHalfOpen(operation);
default:
throw new IllegalStateException("Unknown circuit state");
}
}
private <T> T executeClosed(Supplier<T> operation) throws Exception {
try {
T result = operation.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure(e);
throw e;
}
}
private <T> T executeOpen(Supplier<T> operation) throws Exception {
if (System.currentTimeMillis() - lastFailureTime > timeoutMs) {
state = CircuitState.HALF_OPEN;
return executeHalfOpen(operation);
}
throw new ServiceUnavailableException("Circuit is open");
}
private <T> T executeHalfOpen(Supplier<T> operation) throws Exception {
try {
T result = operation.get();
state = CircuitState.CLOSED;
failureCount = 0;
return result;
} catch (Exception e) {
state = CircuitState.OPEN;
lastFailureTime = System.currentTimeMillis();
throw e;
}
}
private void onSuccess() {
failureCount = 0;
}
private void onFailure(Exception e) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= failureThreshold) {
state = CircuitState.OPEN;
}
}
}
Fallback Behavior Design
For critical external dependencies, fallbacks are implemented:
public class ExternalServiceFallback {
private final CircuitBreaker circuitBreaker;
public ExternalServiceFallback() {
this.circuitBreaker = new CircuitBreaker(5, 30000, 3);
}
public String getExternalData(String key) {
return circuitBreaker.execute(() -> {
// Primary operation
return externalService.getData(key);
});
}
public String getExternalDataWithFallback(String key) {
try {
return circuitBreaker.execute(() -> externalService.getData(key));
} catch (Exception e) {
// Fallback logic
return "default_value_for_" + key;
}
}
}
Error Logging Format and Alerting Thresholds
Structured logging with consistent format:
public class ErrorLogger {
private static final Logger logger = LoggerFactory.getLogger(ErrorLogger.class);
public void logError(String operation, Exception error, Map<String, Object> context) {
Map<String, Object> logContext = new HashMap<>();
logContext.put("operation", operation);
logContext.put("error_code", getErrorCode(error));
logContext.put("error_message", error.getMessage());
logContext.put("timestamp", System.currentTimeMillis());
logContext.putAll(context);
logger.error("Operation failed: {}", operation, error);
}
private String getErrorCode(Exception e) {
if (e instanceof BaseException) {
return ((BaseException) e).getErrorCode();
}
return "UNKNOWN_ERROR";
}
}
Alerting thresholds:
- Critical: 5+ errors per minute for any operation
- Warning: 20+ errors per hour for any operation
- Info: 100+ errors per day for any operation
Error Response Format
Standardized JSON error response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Required field 'name' is missing",
"timestamp": "2023-10-15T14:30:00Z",
"request_id": "req-12345"
}
}
Error Handling in Controller Layer
@RestController
@RequestMapping("/api/v1/builder")
public class BuilderController {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
ErrorResponse error = new ErrorResponse("RESOURCE_NOT_FOUND", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception e) {
ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "Internal server error");
logger.error("Unexpected error occurred", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Error Handling for Builder Creation
public class BuilderCreationService {
private final ErrorLogger errorLogger;
public BuilderResult createBuilder(BuilderRequest request) {
try {
// Validate input
validateRequest(request);
// Create builder instance
Builder builder = createBuilderInstance(request);
// Store in database
Builder stored = builderRepository.save(builder);
return new BuilderResult(stored, "SUCCESS");
} catch (ValidationException e) {
errorLogger.logError("builder_creation_validation", e, Map.of(
"request_id", request.getRequestId(),
"field", e.getMessage()
));
throw e;
} catch (Exception e) {
errorLogger.logError("builder_creation_internal", e, Map.of(
"request_id", request.getRequestId()
));
throw new InternalServerException("Failed to create builder instance", e);
}
}
private void validateRequest(BuilderRequest request) {
if (request.getName() == null || request.getName().trim().isEmpty()) {
throw new ValidationException("Builder name is required");
}
if (request.getComponents() == null || request.getComponents().isEmpty()) {
throw new ValidationException("Builder must have at least one component");
}
}
}
11. Security Design
Input Validation and Sanitization Strategy
All inputs are validated using a combination of:
- Regex-based validation for format constraints (e.g., email, phone number)
- Length limits to prevent buffer overflow attacks
- Type checking to ensure correct data types
- Custom validators for business logic constraints (e.g., valid enum values)
public class InputValidator {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
public static boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
public static String sanitizeString(String input) {
if (input == null) return null;
return input.replaceAll("[^a-zA-Z0-9\\s\\-_.@+]", "");
}
}
SQL Injection Prevention
All database queries use parameterized prepared statements exclusively. No dynamic query construction or string concatenation is permitted.
// ❌ DO NOT DO THIS
String query = "SELECT * FROM users WHERE email = '" + email + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// ✅ DO THIS INSTEAD
String query = "SELECT * FROM users WHERE email = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setString(1, email);
ResultSet rs = stmt.executeQuery();
Authentication Middleware Design
Authentication is handled via JWT tokens with middleware that:
- Verifies token signature using symmetric key (HS256)
- Checks expiration and audience claims
- Extracts user context from token payload
@Component
public class JwtAuthenticationFilter implements Filter {
private final JwtService jwtService;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = extractToken(httpRequest);
if (token != null && jwtService.validateToken(token)) {
String username = jwtService.getUsernameFromToken(token);
Authentication auth = new UsernamePasswordAuthenticationToken(
username, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
Authorization Enforcement
RBAC (Role-Based Access Control) is implemented at service layer:
- Roles are defined in a centralized permissions system
- Each endpoint has role-based access requirements
- Permissions checked before business logic execution
@PreAuthorize("hasRole('ADMIN') or hasRole('USER')")
public void updateUserProfile(@AuthenticationPrincipal User user,
UpdateProfileRequest request) {
// Business logic here
}
Sensitive Data Encryption at Rest and in Transit
- At rest: AES-256-GCM encryption for sensitive fields (SSN, credit cards)
- In transit: TLS 1.3 with HSTS enabled for all endpoints
- Keys managed via AWS KMS (Key Management Service)
@Service
public class DataEncryptionService {
private final AesGcmEncryption aesGcm;
public String encrypt(String plaintext, String keyId) {
return aesGcm.encrypt(plaintext, keyId);
}
public String decrypt(String ciphertext, String keyId) {
return aesGcm.decrypt(ciphertext, keyId);
}
}
PII Handling and Masking Strategy
PII fields (SSN, DOB, email) are:
- Stored encrypted at rest
- Masked in logs and responses (e.g.,
***-**-1234) - Removed from non-essential logs and telemetry
public class PiiMasker {
public static String maskSsn(String ssn) {
return ssn != null ? ssn.replaceAll("(\\d{3})-(\\d{2})-(\\d{4})", "***-**-$3") : null;
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return null;
String[] parts = email.split("@");
return parts[0].substring(0, 2) + "***@" + parts[1];
}
}
Secrets Management
Secrets (API keys, database passwords) are managed through:
- AWS Secrets Manager with automatic rotation
- Environment variables for runtime configuration (e.g., JWT signing key)
- Secrets mounted as files in containerized environments
@Component
public class SecretManager {
private final AWSSecretsManagerClient client;
public String getSecret(String secretName) {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(secretName)
.build();
return client.getSecretValue(request).secretString();
}
}
Audit Logging Design
All security-relevant events are logged with:
- Timestamp, user ID, IP address, operation type
- Event type (login, logout, data access)
- Success/failure status and reason for failures
@Component
public class SecurityAuditLogger {
private final AuditEventRepository auditRepo;
public void logSecurityEvent(String userId, String ipAddress,
String operation, boolean success,
String details) {
AuditEvent event = AuditEvent.builder()
.userId(userId)
.ipAddress(ipAddress)
.operation(operation)
.success(success)
.details(details)
.timestamp(Instant.now())
.build();
auditRepo.save(event);
}
}
12. Testing Strategy
Unit Testing Scope and Coverage Targets (%)
- 90%+ code coverage for core business logic and service layer classes
- 100% test coverage for all utility classes and value objects
- 95%+ coverage for critical path operations (e.g., object creation, validation)
- Target: 85%+ line coverage, 90%+ branch coverage using JaCoCo or similar tools
- Focus areas:
- Builder validation logic (required fields, constraints)
- Object state transitions and immutability
- Error handling paths (invalid inputs, missing fields)
- Concurrent builder usage scenarios
Integration Testing Strategy
- Testcontainers for database integration (PostgreSQL/MySQL) to ensure schema compatibility and query execution
- Embedded Redis for caching integration testing (if caching layer is present)
- WireMock or MockServer for external service integration testing (e.g., auth, notification services)
- In-memory databases (H2) for fast unit test setup with full schema validation
- End-to-end flow tests using embedded Kafka or RabbitMQ for async event handling
Contract Testing
- Pact for consumer-provider contract testing between services (if multiple microservices involved)
- Schema validation using JSON Schema or Protobuf for API contracts
- Versioned contract testing to ensure backward compatibility during API evolution
Performance Testing Tool and Key Scenarios
- k6 or Gatling for load testing of builder endpoints under concurrent usage
- Key scenarios:
- High-concurrency object creation (1000+ concurrent builders)
- Builder with large nested object graphs
- Builder with validation overhead (e.g., regex, size limits)
- Target metrics:
- p95 latency < 100ms for single object creation
- Throughput > 5000 ops/sec under load
- Error rate < 0.1% under stress
Test Data Management Strategy
- Factory patterns for generating test data (e.g.,
UserBuilder,OrderBuilder) - Fixtures for predefined test data sets (e.g., valid user profiles, product catalogs)
- Seeding strategy using database migration scripts or test data loaders
- Test data isolation with unique prefixes or UUIDs to avoid cross-test contamination
- Data cleanup strategy using transaction rollback or test-specific schema drops
// Example of factory pattern for test data generation
public class TestUserBuilder {
public static User buildValidUser() {
return new User.Builder()
.withId(UUID.randomUUID())
.withName("Test User")
.withEmail("test@example.com")
.withAge(25)
.build();
}
}
13. Observability Design
Structured Logging Schema
All logs must follow a consistent schema to enable effective search, filtering, and analysis:
{
"timestamp": "2023-10-15T14:30:45.123Z",
"level": "INFO|WARN|ERROR",
"service": "builder-service",
"trace_id": "1234567890abcdef1234567890abcdef",
"span_id": "abcdef1234567890",
"user_id": "user-123",
"request_id": "req-456",
"component": "BuilderService",
"operation": "createObject",
"entity_id": "builder-789",
"message": "Object creation started",
"error_code": "VALIDATION_ERROR",
"error_details": {
"field": "name",
"message": "Name is required"
},
"duration_ms": 150,
"tags": {
"builder_type": "vehicle",
"object_type": "car"
}
}
PII masking rules:
- User IDs and identifiers are masked in logs when not needed for debugging
- All sensitive fields (passwords, tokens) are completely omitted from logs
- Email addresses are truncated to show only domain part
Distributed Tracing Instrumentation
Using OpenTelemetry for end-to-end tracing:
Custom Spans to Add:
builder.object.creation.start- when builder starts processingbuilder.object.validation- validation phasebuilder.object.build- actual object constructionbuilder.object.persist- persistence operation
Attributes to Capture:
Span span = Span.current();
span.setAttribute("builder.type", builderType);
span.setAttribute("object.class.name", objectClass.getSimpleName());
span.setAttribute("validation.result", validationOutcome);
span.setAttribute("build.duration.ms", buildDurationMs);
Metrics Design
RED Method Metrics:
| Metric Name | Type | Labels | Description |
|---|---|---|---|
builder.objects.created | Counter | builder_type, object_type, status | Total objects created |
builder.validation.errors | Counter | builder_type, validation_rule | Validation failures |
builder.build.duration | Histogram | builder_type, object_type | Build operation latency |
builder.persistence.errors | Counter | builder_type, error_code | Persistence failures |
builder.active_builds | Gauge | builder_type | Concurrent builds in progress |
Label Design:
builder_type: vehicle, document, configobject_type: car, truck, user_document, api_configstatus: success, failure, validation_errorerror_code: VALIDATION_ERROR, DATABASE_ERROR, TIMEOUT
Health Check Endpoint Design
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/live")
public ResponseEntity<Map<String, Object>> live() {
Map<String, Object> response = new HashMap<>();
response.put("status", "healthy");
response.put("timestamp", Instant.now());
return ResponseEntity.ok(response);
}
@GetMapping("/ready")
public ResponseEntity<Map<String, Object>> ready() {
Map<String, Object> response = new HashMap<>();
response.put("status", "ready");
response.put("database", databaseHealth());
response.put("cache", cacheHealth());
response.put("timestamp", Instant.now());
if (databaseHealth() != "healthy" || cacheHealth() != "healthy") {
return ResponseEntity.status(503).body(response);
}
return ResponseEntity.ok(response);
}
private String databaseHealth() {
// Check DB connection pool and basic query
return "healthy";
}
private String cacheHealth() {
// Check Redis connection and basic operation
return "healthy";
}
}
Alerting Thresholds
| Metric | Warning Level | Critical Level | Escalation Path |
|---|---|---|---|
builder.build.duration.p99 | > 500ms | > 2000ms | On-call engineer |
builder.objects.created rate | < 10/sec | < 5/sec | SRE team |
builder.validation.errors rate | > 10/sec | > 50/sec | Product team |
builder.persistence.errors rate | > 1/sec | > 5/sec | DBA + SRE |
builder.active_builds | > 1000 concurrent | > 5000 concurrent | SRE + Platform team |
Service Level Indicators (SLI) - Hypothetical
Based on business requirements:
- Availability SLI: 99.9% uptime (max 8.76 minutes downtime/month)
- Latency SLI: 95% of requests under 200ms
- Success Rate SLI: 99.5% of valid builder operations succeed
Error Budget Management
- Error Budget: 0.5% of total requests
- Budget Burn Rate Alert: When error rate exceeds 0.25% for 5 minutes
- Auto-remediation: When error rate exceeds 0.75% for 10 minutes, trigger alert and pause new builds temporarily
Observability Tooling Stack
- Logging: ELK stack (Elasticsearch, Logstash, Kibana) or CloudWatch + Fluentd
- Tracing: Jaeger or OpenTelemetry Collector with Zipkin backend
- Metrics: Prometheus + Grafana dashboard for real-time monitoring
- Alerting: Alertmanager + PagerDuty integration for incident response
Log Rotation and Retention
- Log Rotation: Daily rotation with 30-day retention
- Archive Strategy: Compressed logs retained for 90 days for compliance
- Log Aggregation: Centralized logging with structured JSON format for searchability
Service Monitoring Dashboard
Dashboard includes:
- Real-time build success/failure rates
- Latency distributions (p50, p95, p99)
- Concurrent build capacity
- Error rate trends over time
- Builder type performance breakdown
Audit Trail Integration
All builder operations are logged with:
- Timestamp of operation
- User or system that initiated the build
- Builder type and configuration used
- Object ID and metadata
- Validation results and any errors
This observability design ensures comprehensive monitoring of the builder system's performance, reliability, and operational health while maintaining security and compliance standards.
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Builder vs. Factory pattern for object creation: The choice between a fluent builder and factory-based approach requires alignment on whether to support complex object construction chains or simpler, type-safe creation patterns. This decision affects developer experience and code complexity.
- Immutable vs. mutable object design: The system must decide whether built objects should be immutable (with copy-on-write semantics) or mutable (with in-place updates), impacting performance characteristics and thread safety considerations.
- Validation strategy: Whether validation should occur during build time or at object usage time, and how strict validation rules should be enforced (e.g., runtime vs. compile-time checks).
- Extensibility model: Whether the builder system should support extension through plugins or inheritance, and how to handle versioning of builder interfaces.
Known trade-offs and justifications
| Trade-off | Option A | Option B | Justification |
|---|---|---|---|
| Builder flexibility vs. type safety | Fluent builder with generic parameters | Type-safe factory methods | Fluent builders provide better discoverability but can be harder to type-check; factory methods offer compile-time validation at cost of verbosity |
| Builder composition vs. inheritance | Composition-based builder hierarchy | Inheritance-based builder hierarchy | Composition offers better testability and avoids diamond inheritance problems; inheritance provides cleaner code but increases coupling |
| Runtime vs. compile-time validation | Runtime validation with exceptions | Compile-time validation with generics | Runtime validation provides more flexibility but can lead to runtime errors; compile-time validation prevents errors but requires more upfront design effort |
| Builder performance vs. memory usage | Memory-efficient builder with minimal state tracking | Memory-heavy builder with full object graph tracking | Memory-efficient builders reduce overhead but may require more complex state management; full tracking enables better validation but increases memory footprint |
Risks and mitigation strategies
- API drift risk: As the builder system evolves, maintaining backward compatibility across different builder versions could lead to API drift. Mitigation: Implement strict versioning and semantic versioning policies with automated breaking change detection.
- Performance degradation: Complex builder chains or excessive validation logic could impact object creation performance. Mitigation: Profile key builder paths, implement lazy validation where appropriate, and provide performance benchmarks for different usage patterns.
- Thread safety issues: Concurrent object creation using shared builders could introduce race conditions. Mitigation: Design builders to be stateless or use thread-local instances, and document thread safety guarantees clearly.
- Builder explosion: Too many builder variants could lead to combinatorial complexity. Mitigation: Implement a modular builder design where builders can be composed and reused, and provide clear documentation on when to use each variant.
- Testing complexity: Builders with complex validation logic can be hard to test comprehensively. Mitigation: Provide extensive unit tests for each builder variant, use property-based testing for validation logic, and implement integration tests for end-to-end builder flows.
Technical debt acknowledged
- Builder interface bloat: The current design may lead to an explosion of builder interfaces over time, especially if each domain object requires its own builder variant. This creates maintenance overhead and increases cognitive load for developers.
- Validation logic duplication: Different builder implementations may end up duplicating validation logic, leading to maintenance challenges. This is mitigated by using shared validation components but requires careful design to avoid redundancy.
- Type parameter complexity: The use of generic type parameters in builder chains can make the API harder to understand and use for developers unfamiliar with advanced Java generics. This is a trade-off between type safety and developer accessibility.
- Versioning complexity: As the system evolves, maintaining backward compatibility while introducing new builder features can become complex and error-prone. This debt is managed through careful versioning strategies but requires ongoing attention.
- Testing coverage gaps: Some edge cases in builder construction chains may not be adequately covered by unit tests, particularly around concurrent usage and error conditions. These gaps are acknowledged but require ongoing attention to maintain system reliability.