Adopting the 7-Layer Effect-TS Architecture in Holochain Projects
A comprehensive guide for implementing the proven 7-layer Effect-TS architecture in new or existing Holochain projects.
๐ Overview
This guide explains how to adopt the sophisticated 7-layer Effect-TS architecture from the Requests & Offers project in your own Holochain applications. This architecture has been proven to provide exceptional maintainability, developer experience, and production reliability.
Target Audience
- New Holochain Projects: Starting fresh with best practices
- Existing Projects: Migrating from simpler architectures
- Development Teams: Establishing consistent patterns
- Enterprise Applications: Production-grade reliability requirements
๐๏ธ Architecture Overview
The 7 Layers
- Service Layer: Effect-native services with Context.Tag dependency injection
- Store Layer: Svelte 5 Runes with standardized helper functions
- Schema Layer: Effect Schema validation with strategic boundaries
- Error Layer: Domain-specific tagged errors with centralized management
- Composable Layer: Effect-based business logic abstraction
- Component Layer: Svelte 5 components with accessibility focus
- Test Layer: Comprehensive testing across all layers
Key Benefits
- ๐ Consistency: Predictable patterns across all domains
- ๐งช Type Safety: End-to-end typing from zome to UI
- ๐ Developer Experience: Rapid onboarding and feature development
- ๐ก๏ธ Reliability: Graceful error handling and recovery
- ๐ Maintainability: Clear separation of concerns
๐ฏ Decision Guide: Should You Adopt This Architecture?
โ Perfect Fit For
| Project Type | Complexity | Team Size | Timeline | Recommendation |
|---|---|---|---|---|
| Multi-domain hApp | High | 3+ developers | 6+ months | Full Adoption |
| Enterprise Integration | High | 2+ developers | 4+ months | Full Adoption |
| Production Application | Medium-High | 1+ developers | 3+ months | Strong Consider |
| hREA Integration | Medium | 1+ developers | 2+ months | Recommended |
โ ๏ธ Consider Alternatives For
| Project Type | Complexity | Team Size | Timeline | Recommendation |
|---|---|---|---|---|
| Simple CRUD App | Low | 1 developer | <1 month | Simplified Approach |
| Quick Prototype | Low | 1 developer | <2 weeks | Direct Svelte |
| Learning Project | Low | 1 developer | Variable | Start Simple |
| Single Domain | Low-Medium | 1-2 developers | <3 months | Hybrid Approach |
๐ Implementation Strategies
Strategy 1: Full Adoption (New Projects)
Phase 1: Foundation Setup (1-2 weeks)
# 1. Initialize project structure
mkdir -p my-happ/ui/src/lib/{services,stores,composables,components,schemas,errors,utils}
mkdir -p my-happ/ui/src/lib/utils/store-helpers
mkdir -p my-happ/ui/src/lib/composables/domain
mkdir -p my-happ/ui/src/lib/components/ui
# 2. Install core dependencies
cd my-happ/ui
bun add effect @effect/schema @holochain/client
bun add svelte @sveltejs/kit
bun add vitest @vitest/ui @playwright/test
# 3. Copy essential utilities
# From this project, copy:
# - ui/src/lib/utils/store-helpers/ (entire directory)
# - ui/src/lib/errors/ (error handling patterns)
# - ui/src/lib/utils/effect.ts (Effect utilities)
Phase 2: Service Layer Implementation (1 week per domain)
// 1. Create service template (reference: serviceTypes.service.ts)
export const MyDomainService = Context.GenericTag<MyDomainService>("MyDomainService");
export const makeMyDomainService = Effect.gen(function* () {
const client = yield* HolochainClientService;
const createMyEntity = (input: CreateMyEntityInput) =>
Effect.gen(function* () {
// Implementation
});
return { createMyEntity };
});
Phase 3: Store Layer Implementation (1 week per domain)
// 1. Implement 9 standardized helpers
export const createMyDomainStore = () => {
let entities = $state<MyEntity[]>([]);
// 1. Entity Creation Helper
const createUIEntity = createUIEntityFromRecord<MyEntityInDHT, UIMyEntity>(
(entry, actionHash, timestamp) => ({ ...entry, original_action_hash: actionHash, created_at: timestamp })
);
// 2. Record Mapping Helper
const mapRecordsToUIEntities = (records: HolochainRecord[]) =>
records.map(record => createUIEntity(record)).filter(Boolean);
// 3-9. Implement remaining helpers...
return { entities: () => entities, /* other methods */ };
};
Phase 4: Composable Layer (3-5 days per domain)
// 1. Create domain composable
export function useMyDomainManagement() {
const store = myDomainStore;
const service = yield* MyDomainService;
const createEntity = (input: CreateMyEntityInput) =>
Effect.gen(function* () {
const result = yield* service.createMyEntity(input);
// Update store, emit events, etc.
});
return { createEntity };
}
Strategy 2: Hybrid Approach (Existing Projects)
Phase 1: Assessment (2-3 days)
-
Audit Current Architecture
// Document existing patterns - Current state management approach - Error handling strategies - Component organization - Testing practices -
Identify Migration Candidates
// Start with most complex domain - Domains with multiple state interactions - Areas with frequent bugs - New features being added
Phase 2: Incremental Migration (2-3 weeks per domain)
// Step 1: Add Effect-TS dependencies
bun add effect @effect/schema
// Step 2: Implement service layer for one domain
export const ExistingDomainService = Context.GenericTag<ExistingDomainService>("ExistingDomainService");
// Step 3: Keep existing components, add service integration
function ExistingComponent() {
// Gradually replace old patterns with new
const { createEntity } = useExistingDomainService();
}
Phase 3: Pattern Standardization (1-2 weeks)
// 1. Adopt store helpers gradually
import { createGenericCacheSyncHelper } from '$lib/utils/store-helpers';
// 2. Implement error boundaries
import { useErrorBoundary } from '$lib/composables/ui/useErrorBoundary.svelte';
// 3. Add testing infrastructure
import { createMockService } from '$lib/utils/mocks';
Strategy 3: Minimal Integration (Quick Start)
Essential Elements Only (1 week implementation)
// 1. Add only critical utilities
// Service layer pattern
export const SimpleService = Context.Tag<SimpleService>();
// Basic store helper
const withLoadingState = (operation) => (setLoading, setError) =>
pipe(
operation,
tap(() => setLoading(true)),
catchError((error) => {
setError(error.message);
return E.fail(error);
}),
finally(() => setLoading(false))
);
// 2. Keep existing component structure
function MyComponent() {
const loading = $state(false);
const error = $state(null);
const createEntity = withLoadingState(
service.createEntity(input)
)(setLoading, setError);
}
๐ File Structure Templates
New Project Structure
my-happ/
โโโ dnas/
โ โโโ my_domain/
โ โโโ zomes/
โ โ โโโ coordinator/
โ โ โโโ integrity/
โโโ ui/
โโโ src/
โ โโโ lib/
โ โ โโโ services/
โ โ โ โโโ HolochainClientService.ts
โ โ โ โโโ zomes/
โ โ โ โ โโโ myDomain.service.ts
โ โ โโโ stores/
โ โ โ โโโ myDomain.store.svelte.ts
โ โ โโโ composables/
โ โ โ โโโ domain/
โ โ โ โ โโโ myDomain/
โ โ โ โ โโโ useMyDomainManagement.svelte.ts
โ โ โโโ components/
โ โ โ โโโ ui/
โ โ โ โโโ myDomain/
โ โ โโโ schemas/
โ โ โ โโโ myDomain.schema.ts
โ โ โโโ errors/
โ โ โ โโโ myDomain.error.ts
โ โ โ โโโ error-contexts.ts
โ โ โโโ utils/
โ โ โโโ store-helpers/
โ โ โโโ effect.ts
โ โโโ routes/
โโโ tests/
โโโ unit/
โโโ integration/
โโโ e2e/
Essential Files to Copy
Copy these core files from the Requests & Offers project:
# Core utilities
cp -r ui/src/lib/utils/store-helpers/ my-project/ui/src/lib/utils/
cp ui/src/lib/utils/effect.ts my-project/ui/src/lib/utils/
cp ui/src/lib/utils/mocks.ts my-project/ui/src/lib/utils/
# Error handling foundation
cp ui/src/lib/errors/error-contexts.ts my-project/ui/src/lib/errors/
cp ui/src/lib/errors/base.error.ts my-project/ui/src/lib/errors/
# Service patterns
cp ui/src/lib/services/HolochainClientService.ts my-project/ui/src/lib/services/
# Store events
cp ui/src/lib/stores/storeEvents.ts my-project/ui/src/lib/stores/
๐ ๏ธ Implementation Checklist
Pre-Implementation
- Architecture Decision: Choose adoption strategy (Full/Hybrid/Minimal)
- Team Alignment: Ensure team understands Effect-TS concepts
- Dependency Planning: Budget 2-4 weeks for learning curve
- Tool Setup: Install Bun, Effect-TS, testing infrastructure
Foundation Layer
- Package Structure: Establish consistent directory layout
- Core Dependencies: Install Effect-TS, schemas, testing tools
- Utility Functions: Copy store-helpers and Effect utilities
- Error Framework: Implement base error classes and contexts
Service Layer
- Service Template: Create Context.Tag service pattern
- Dependency Injection: Implement service composition
- Error Handling: Add domain-specific error types
- Holochain Integration: Connect service layer to zome functions
Store Layer
- State Management: Implement Svelte 5 Runes pattern
- Helper Functions: Implement all 9 standardized helpers
- Cache Management: Add EntityCache with TTL
- Event Integration: Connect to store event system
Composable Layer
- Business Logic: Extract to Effect-based composables
- Error Boundaries: Implement retry mechanisms
- State Synchronization: Ensure cache/state consistency
- User Experience: Add loading states and feedback
Component Layer
- Accessibility: Implement ARIA patterns and keyboard navigation
- Composable Integration: Use composables for business logic
- Responsive Design: Mobile-first approach
- Error Display: User-friendly error messages
Test Layer
- Unit Tests: Service layer with comprehensive mocking
- Integration Tests: Store and composable interactions
- E2E Tests: User workflows with Playwright
- Backend Tests: Holochain zome testing with Sweettest
๐งช Migration Patterns
Existing State Migration
// Before: Simple Svelte store
let entities = writable([]);
// After: Effect-based store with helpers
export const createMyDomainStore = () => {
let entities = $state<MyEntity[]>([]);
const { syncCacheToState } = createGenericCacheSyncHelper({
all: entities
});
const fetchEntities = withLoadingState(() =>
pipe(
myDomainService.getAllEntities(),
E.map(mapRecordsToUIEntities),
E.tap((processed) => {
entities.splice(0, entities.length, ...processed);
})
)
);
return { entities: () => entities, fetchEntities };
};
Error Handling Migration
// Before: Basic try/catch
try {
await createEntity(input);
} catch (error) {
console.error('Failed:', error);
}
// After: Effect-based error handling
const createEntityEffect = (input: CreateEntityInput) =>
pipe(
myDomainService.createEntity(input),
E.catchAll((error) =>
E.fail(MyDomainError.fromError(error, ERROR_CONTEXTS.CREATE_ENTITY))
)
);
๐ Success Metrics
Quality Metrics
- Test Coverage: โฅ80% unit, โฅ70% integration
- Type Safety: 100% TypeScript coverage
- Error Boundaries: All user operations covered
- Performance: <100ms response time for operations
Developer Experience
- Onboarding Time: <1 week for new developers
- Feature Addition: <3 days for new domain implementation
- Bug Rate: <50% reduction in production bugs
- Code Review Time: <30 minutes per PR
Production Metrics
- Uptime: โฅ99.9% for critical operations
- Error Recovery: Automatic recovery for 90% of errors
- Bundle Size: <500KB initial load
- Memory Usage: <100MB for typical sessions
๐ Learning Resources
Essential Concepts
-
Effect-TS Fundamentals
- Effect.gen vs .pipe decision matrix
- Context.Tag dependency injection
- Error handling with Either and Exit
-
Svelte 5 Runes
- $state, $derived, $effect reactivity
- Component composition patterns
- Accessibility best practices
-
Holochain Integration
- Zome function patterns
- DHT operation understanding
- hREA integration concepts
Practice Exercises
-
Start with Simple Domain
- Implement basic CRUD operations
- Add store helpers incrementally
- Build components with composables
-
Progress to Complex Domain
- Multi-entity relationships
- Cross-domain communication
- Advanced error scenarios
-
Master Production Patterns
- Performance optimization
- Error recovery strategies
- Testing comprehensive coverage
๐ง Troubleshooting
Common Challenges
Effect-TS Learning Curve
// Problem: Understanding when to use Effect.gen vs .pipe
// Solution: Use decision matrix from development guidelines
// Effect.gen: Dependencies, conditional logic, sequential operations
Effect.gen(function* () {
const service = yield* MyService;
if (condition) {
return yield* service.methodA();
} else {
return yield* service.methodB();
}
});
// .pipe: Error handling, tracing, simple transforms
pipe(
service.method(),
E.map(transform),
E.catchAll(handleError)
);
Type Complexity
// Problem: Complex Effect types can be intimidating
// Solution: Let TypeScript inference do the work
// Instead of explicit types:
const myFunction: Effect<string, Error, MyService> = ...
// Let inference work:
const myFunction = Effect.gen(function* () {
const service = yield* MyService;
return service.getString();
});
Performance Issues
// Problem: Too many re-renders or slow updates
// Solution: Use derived values and batch operations
// Good: $derived for computed values
const filteredEntities = $derived(
entities.filter(e => e.status === 'active')
);
// Good: Batch updates with cache helpers
const { syncCacheToState } = createGenericCacheSyncHelper({
all: entities,
pending: pendingEntities
});
Performance Optimization
-
Bundle Size
// Use dynamic imports for large features const HeavyComponent = lazy(() => import('./HeavyComponent.svelte')); -
Memory Management
// Clear cache on navigation $effect(() => { return () => { cache.clear(); }; }); -
Response Time
// Use optimistic updates const createEntity = (input) => pipe( service.createEntity(input), E.tap((entity) => { // Update UI immediately entities.push(entity); }) );
๐ฏ Conclusion
The 7-layer Effect-TS architecture represents a significant investment in code quality and maintainability. While it adds complexity compared to simpler approaches, the dividends in long-term maintainability, developer experience, and production reliability make it an excellent choice for serious Holochain applications.
Start small, iterate, and gradually adopt patterns as you understand their value. The architecture is designed to be adopted incrementally, allowing you to benefit from its advantages without requiring a complete rewrite of existing code.
For projects planning to scale, having multiple domains, or requiring production reliability, this architecture provides a solid foundation that will serve your project well throughout its lifecycle.
Next Steps:
- Assess your project's complexity and requirements
- Choose an adoption strategy
- Implement the foundation layer
- Gradually adopt patterns domain by domain
- Establish testing and quality metrics
- Iterate and improve based on experience
Remember: The goal is not perfection, but consistent improvement in code quality and developer experience.