Dependency Management
Easy AppServer provides dependency management to handle relationships between applications, ensuring proper installation order and safe uninstallation with impact analysis. Version constraint metadata is persisted today but is not validated during installation yet.
Overview
Applications can declare dependencies on other applications, ensuring that:
- Required apps are installed before dependent apps
- Version constraints are satisfied
- Uninstalling apps doesn't break dependents
- Dependency graphs are validated and cycle-free
- Installation order respects the dependency topology
Code Reference: pkg/v2/domain/dependency/dependency.go:19
Dependency Types
HARD Dependencies
A HARD dependency means the application cannot function without the dependency being present and healthy.
Code Reference: pkg/v2/domain/dependency/dependency.go:19
type Dependency struct {
AppID uuid.UUID // The dependent app
DependsOnAppID uuid.UUID // The dependency
Kind Kind // HARD or SOFT
VersionConstraint string // e.g., ">=1.0.0 <2.0.0"
CreatedAt time.Time
}
Characteristics
- Installation fails if dependency is not met
- Dependency must be installed first (enforced by platform)
- Uninstalling dependency is blocked if dependents exist
- Version constraints are stored but not currently enforced (planned)
- Platform validates dependency existence and cycles before allowing installation
Use Cases
// Manifest with HARD dependencies
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^2.0.0' })
.dependency({ appName: 'de.easy-m.data', required: true, minVersion: '>=1.5.0' })
Examples:
- Shared authentication service
- Core data service required for business logic
- Platform API that the app extends
- Required infrastructure services (logging, monitoring)
SOFT Dependencies
A SOFT dependency means the application prefers the dependency but can function without it with graceful degradation.
Characteristics
- Installation succeeds even if dependency is not available
- Application must handle missing dependency gracefully
- Provides enhanced features when dependency is present
- Version constraints are recommendations (warnings only)
- Uninstalling dependency proceeds without blocking
Use Cases
// Manifest with SOFT dependencies
.dependency({ appName: 'de.easy-m.analytics', required: false, minVersion: '^1.0.0' })
.dependency({ appName: 'de.easy-m.search', required: false, minVersion: '>=2.0.0' })
Examples:
- Optional integrations
- Enhanced features (search, analytics)
- Monitoring or observability integrations
- Third-party service integrations
Dependency Declaration
Code Reference: web/v2/packages/appserver-sdk/src/manifest/builder.ts:1
import { defineManifest } from '@easy/appserver-sdk';
const manifest = defineManifest()
.name('de.easy-m.billing')
.certificate(certificatePEM)
// HARD dependencies (required)
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^2.0.0' })
.dependency({ appName: 'de.easy-m.customers', required: true, minVersion: '>=1.0.0' })
// SOFT dependencies (optional)
.dependency({ appName: 'de.easy-m.analytics', required: false, minVersion: '^1.0.0' })
.dependency({ appName: 'de.easy-m.notifications', required: false, minVersion: '>=1.0.0' })
.build();
Version Constraints
Dependencies can specify semantic version requirements:
Constraint Syntax
Exact version: "1.2.3"
Minimum version: ">=1.2.0"
Version range: ">=1.0.0 <2.0.0"
Compatible version: "~1.2.0" // Allows: 1.2.0, 1.2.1, 1.2.2 (patch updates)
Minor version: "^1.2.0" // Allows: 1.2.0, 1.3.0, 1.9.9 (minor + patch)
Semantic Versioning
AppServer follows semver (semantic versioning):
- Major (1.x.x): Breaking changes, incompatible API changes
- Minor (x.1.x): New features, backward compatible additions
- Patch (x.x.1): Bug fixes, backward compatible fixes
Version Constraint Examples
// Flexible: Allow any 1.x version
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^1.0.0' })
// Restrictive: Exact major version range
.dependency({ appName: 'de.easy-m.data', required: true, minVersion: '>=2.0.0' })
// Patch-only: Only allow patch updates
.dependency({ appName: 'de.easy-m.core', required: true, minVersion: '~1.5.0' })
// Minimum: Any version >= 1.0.0
.dependency({ appName: 'de.easy-m.utils', required: false, minVersion: '>=1.0.0' })
Version constraints are stored with each dependency but the current
DependencyResolveronly validates app existence and cycle detection; it does not enforce semantic version ranges yet.
Dependency Resolver
The platform uses a DependencyResolver to validate and analyze dependencies.
Code Reference: pkg/v2/domain/service/dependency_resolver.go:25
type DependencyResolver interface {
// ValidateDependencies checks for cycles and validates referenced apps exist
ValidateDependencies(ctx context.Context, appID uuid.UUID, deps []*dependency.Dependency) error
// CheckInstallPreconditions ensures all HARD dependencies are installed
CheckInstallPreconditions(ctx context.Context, appID uuid.UUID) error
// CheckUninstallImpact finds apps that depend on the given app
CheckUninstallImpact(ctx context.Context, appID uuid.UUID) (dependents []uuid.UUID, hasHard bool, err error)
// DetectCycles checks if adding dependencies would create a cycle
DetectCycles(ctx context.Context, graph map[uuid.UUID][]uuid.UUID) error
// GetInstallationOrder returns apps in dependency order (topological sort)
GetInstallationOrder(ctx context.Context, appIDs []uuid.UUID) ([]uuid.UUID, error)
}
Validation During Registration
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:159
// During app registration, validate dependencies
if len(cmd.Dependencies) > 0 {
if err := s.dependencyResolver.ValidateDependencies(ctx, newApp.ID, cmd.Dependencies); err != nil {
return nil, fmt.Errorf("dependency validation failed: %w", err)
}
}
Registration validates that:
- All referenced dependency apps exist
- No cycles are created
- Dependency depth doesn't exceed max (default: 50)
- No self-dependencies
Installation Precondition Checks
Code Reference: pkg/v2/domain/service/dependency_resolver.go:125
func (r *dependencyResolver) CheckInstallPreconditions(ctx context.Context, appID uuid.UUID) error {
// Get app's dependencies
deps, err := r.depRepo.FindByAppID(ctx, appID)
// Filter HARD dependencies
var hardDeps []*dependency.Dependency
for _, dep := range deps {
if dep.IsHard() {
hardDeps = append(hardDeps, dep)
}
}
// Verify each HARD dependency is installed
for _, dep := range hardDeps {
if !depApp.IsInstalled() {
return &DependencyUnhealthyError{
DependencyName: depApp.Name,
State: depApp.State.String(),
}
}
}
return nil
}
Installation fails if:
- Any HARD dependency is not installed
- Any HARD dependency is in unhealthy state
- Dependency cycles are detected
Version constraint validation is not currently enforced (pkg/v2/domain/service/dependency_resolver.go:83-188 only checks existence, cycles, and installed state). Version constraints declared in manifests are persisted but not validated during installation. Semantic version range enforcement is planned for future implementation.
Uninstallation Impact Analysis
Code Reference: pkg/v2/domain/service/dependency_resolver.go:191
func (r *dependencyResolver) CheckUninstallImpact(ctx context.Context, appID uuid.UUID) ([]uuid.UUID, bool, error) {
// Find apps that depend on this app
dependents, err := r.depRepo.FindDependents(ctx, appID)
// Collect dependent app IDs and check for HARD dependencies
dependentIDs := make([]uuid.UUID, len(dependents))
hasHard := false
for i, dep := range dependents {
dependentIDs[i] = dep.AppID
if dep.IsHard() {
hasHard = true
}
}
return dependentIDs, hasHard, nil
}
If hasHard = true, uninstallation is blocked unless force: true flag is set.
Cycle Detection
The resolver uses Depth-First Search (DFS) to detect circular dependencies.
Code Reference: pkg/v2/domain/service/dependency_resolver.go:217
DFS Algorithm
func (r *dependencyResolver) DetectCycles(ctx context.Context, graph map[uuid.UUID][]uuid.UUID) error {
visited := make(map[uuid.UUID]bool)
recStack := make(map[uuid.UUID]bool)
var path []uuid.UUID
var dfs func(uuid.UUID, int) error
dfs = func(node uuid.UUID, depth int) error {
// Check max depth
if depth > r.maxDepth {
return fmt.Errorf("dependency depth exceeds maximum of %d", r.maxDepth)
}
// If in recursion stack, we found a cycle
if recStack[node] {
cycle := append(path[cycleStart:], node)
return &CyclicDependencyError{Cycle: cycle}
}
// Already visited in previous DFS path
if visited[node] {
return nil
}
// Mark as visited and add to recursion stack
visited[node] = true
recStack[node] = true
path = append(path, node)
// Visit all dependencies
for _, dep := range graph[node] {
if err := dfs(dep, depth+1); err != nil {
return err
}
}
// Remove from recursion stack
recStack[node] = false
return nil
}
// Run DFS from each node
for node := range graph {
if !visited[node] {
if err := dfs(node, 0); err != nil {
return err
}
}
}
return nil
}
Cycle Detection Example
Invalid Cycle:
app-a depends on app-b
app-b depends on app-c
app-c depends on app-a ← CYCLE DETECTED
Error: CyclicDependencyError{Cycle: [app-a, app-b, app-c, app-a]}
The platform rejects registration if cycles are detected.
Topological Sorting
The resolver computes installation order using Kahn's algorithm for topological sorting.
Code Reference: pkg/v2/domain/service/dependency_resolver.go:281
Kahn's Algorithm
func (r *dependencyResolver) GetInstallationOrder(ctx context.Context, appIDs []uuid.UUID) ([]uuid.UUID, error) {
// Get dependency graph
graph, err := r.depRepo.GetDependencyGraphForApps(ctx, appIDs)
// Build reverse graph: if A depends on B, add edge B -> A
reverseGraph := make(map[uuid.UUID][]uuid.UUID)
inDegree := make(map[uuid.UUID]int)
for appID, deps := range graph {
for _, depID := range deps {
reverseGraph[depID] = append(reverseGraph[depID], appID)
inDegree[appID]++
}
}
// Start with nodes that have in-degree 0 (no dependencies)
var queue []uuid.UUID
for id, degree := range inDegree {
if degree == 0 {
queue = append(queue, id)
}
}
// Process nodes in dependency order
var result []uuid.UUID
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Reduce in-degree for dependents
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
return result, nil
}
Installation Order Example
Dependencies:
- app-a: no dependencies
- app-b: depends on app-a
- app-c: depends on app-a, app-b
- app-d: depends on app-c
Installation Order: [app-a, app-b, app-c, app-d]
Apps are installed in order, ensuring dependencies are always installed first.
Dependency Graph
The platform maintains a directed acyclic graph (DAG) of all application dependencies.
Graph Structure
app-auth (no dependencies)
↑
|
app-data (depends on app-auth)
↑
|
app-billing (depends on app-data)
↑
|
app-reporting (depends on app-billing)
Graph Operations
Impact Analysis
Before uninstalling app-data, find dependents:
app-data (being uninstalled)
↑ HARD
|
app-billing ← Will be blocked from installation
↑ HARD
|
app-reporting ← Will also be blocked
Result: Uninstallation of app-data is blocked because it has HARD dependents.
Dependency Chain
Following dependencies transitively:
app-reporting
→ app-billing (HARD)
→ app-data (HARD)
→ app-auth (HARD)
Required installations: [app-auth, app-data, app-billing, app-reporting]
Installation Behavior
HARD Dependency Not Met
Scenario: Install app-billing, but app-data is not installed
Result:
✗ Installation fails with error
✗ User notified: "HARD dependency not met: app-data (required: >=1.0.0)"
✓ Suggestion: "Please install app-data first"
Code Reference: pkg/v2/domain/service/dependency_resolver.go:125
SOFT Dependency Not Met
Scenario: Install app-analytics, but app-search is not installed
Result:
✓ Installation succeeds with warning
⚠ User notified: "SOFT dependency missing: app-search (recommended: >=1.0.0)"
✓ App handles missing dependency gracefully
Version Mismatch
Scenario: app-billing requires app-data ^2.0.0, but app-data 1.5.0 is installed
HARD Dependency:
✗ Installation fails
✗ Error: "Version constraint not satisfied: app-data (required: ^2.0.0, installed: 1.5.0)"
✓ Suggestion: "Please upgrade app-data to version >=2.0.0"
SOFT Dependency:
✓ Installation succeeds with warning
⚠ Warning: "SOFT dependency version mismatch: app-search (recommended: >=2.0.0, installed: 1.0.0)"
Version-based failures and warnings are part of the planned behavior. The current
DependencyResolveronly checks that dependency apps exist and are installed; it does not validate semantic versions.
Uninstallation Behavior
App Has HARD Dependents
Scenario: Uninstall app-data, but app-billing depends on it (HARD)
Result:
✗ Uninstallation blocked
✗ Error: "Cannot uninstall app-data: HARD dependents exist"
✓ Dependent list: ["app-billing", "app-reporting"]
✓ Options:
1. Uninstall dependents first
2. Use force flag (dangerous)
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:669
// Check for dependents
if !cmd.Force {
dependentIDs, hasHard, err := s.dependencyResolver.CheckUninstallImpact(ctx, foundApp.ID)
if hasHard && len(dependentIDs) > 0 {
return nil, &HasDependentsError{
AppName: foundApp.Name,
Dependents: dependentNames,
}
}
}
App Has SOFT Dependents
Scenario: Uninstall app-search, app-analytics depends on it (SOFT)
Result:
✓ Uninstallation proceeds
✓ Dependent apps notified via event: "dependency.uninstalled"
✓ app-analytics continues running with degraded functionality
Force Uninstall
# Force uninstall (ignores HARD dependents)
uninstallApp(appID, { force: true })
Warning: Force uninstall will break dependent applications. Use with caution.
Runtime Dependency Checking
Applications can check dependency availability at runtime.
SDK Helpers
import { checkDependency } from '@easy/appserver-sdk';
async function sendNotification(user, message) {
// Check if optional notifications service is available
const hasNotifications = await checkDependency('de.easy-m.notifications');
if (hasNotifications) {
// Use notifications service
await notifications.send(user, message);
} else {
// Fallback: Send email instead
await sendEmail(user.email, message);
}
}
Graceful Degradation
class AnalyticsService {
private searchAvailable: boolean = false;
async initialize() {
// Check SOFT dependency
this.searchAvailable = await checkDependency('de.easy-m.search');
if (!this.searchAvailable) {
logger.warn('Search service not available - using basic analytics');
}
}
async generateReport(filters) {
if (this.searchAvailable) {
// Enhanced report with full-text search
return await generateAdvancedReport(filters);
} else {
// Basic report without search
return await generateBasicReport(filters);
}
}
}
Dependency Updates
Compatible Update
Scenario: app-data updates from 1.5.0 to 1.6.0 (minor version)
Dependencies:
- app-billing requires app-data ^1.5.0 ✓ Compatible
- app-reporting requires app-data >=1.0.0 ✓ Compatible
Result:
✓ Update proceeds
✓ Dependent apps continue working
✓ Optional: Restart dependents to use new features
Breaking Update
Scenario: app-data updates from 1.9.0 to 2.0.0 (major version, breaking)
Dependencies:
- app-billing requires app-data ^1.5.0 ✗ Not compatible
- app-reporting requires app-data >=1.0.0 ✓ Still compatible
Result:
⚠ Platform warns about compatibility
✗ app-billing may break (version constraint not met)
✓ Suggestion: "Update app-billing to support app-data 2.x"
Update Strategy
Recommended Approach:
1. Update dependency (app-data 1.9.0 → 2.0.0)
2. Platform detects breaking change
3. Platform lists affected dependents
4. Operator updates dependent apps
5. Operator verifies compatibility
6. All apps running on new version
Best Practices
Declaring Dependencies
// Good: Use required=true for critical dependencies
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^2.0.0' })
// Good: Use required=false for optional features
.dependency({ appName: 'de.easy-m.analytics', required: false, minVersion: '^1.0.0' })
// Bad: Everything as required (over-constrained)
.dependency({ appName: 'de.easy-m.search', required: true, minVersion: '^1.0.0' }) // Should be optional
// Bad: No version constraints
.dependency({ appName: 'de.easy-m.data', required: true }) // Specify minVersion!
Version Constraints
// Good: Flexible ranges
.dependency({ appName: 'de.easy-m.core', required: true, minVersion: '^1.0.0' }) // Allows 1.x.x
// Good: Major version range
.dependency({ appName: 'de.easy-m.api', required: true, minVersion: '>=2.0.0' })
// Bad: Exact version (too restrictive)
.dependency({ appName: 'de.easy-m.utils', required: true, minVersion: '=1.2.3' })
// Bad: No upper bound (risky)
.dependency({ appName: 'de.easy-m.lib', required: true, minVersion: '>=1.0.0' }) // Could break on 2.0.0
Semantic Versioning
Your App Versioning:
- Breaking changes: Increment MAJOR (1.0.0 → 2.0.0)
- New features: Increment MINOR (1.0.0 → 1.1.0)
- Bug fixes: Increment PATCH (1.0.0 → 1.0.1)
Document breaking changes clearly:
- CHANGELOG.md with migration guides
- Deprecation warnings before breaking changes
- Version compatibility matrix
Graceful Degradation
// Good: Handle missing SOFT dependency
async function trackEvent(event) {
if (await hasDependency('analytics')) {
await analytics.track(event);
} else {
logger.info('Analytics unavailable, skipping tracking');
}
}
// Bad: Assume dependency is present
async function trackEvent(event) {
await analytics.track(event); // Crashes if analytics not installed
}
Testing
describe('Billing App', () => {
it('should work with all dependencies', async () => {
// Test with app-auth, app-data installed
await installDependencies(['auth', 'data']);
const result = await app.createInvoice(customer);
expect(result.success).toBe(true);
});
it('should handle missing SOFT dependency', async () => {
// Test without optional analytics
await uninstallDependency('analytics');
const result = await app.createInvoice(customer);
expect(result.success).toBe(true);
expect(result.analyticsTracked).toBe(false);
});
it('should fail without HARD dependency', async () => {
// Test without required auth service
await uninstallDependency('auth');
await expect(app.install()).rejects.toThrow('HARD dependency not met');
});
});
Troubleshooting
Installation Fails - Missing Dependency
Problem: InstallApp fails with "HARD dependency not met"
Diagnosis:
1. Check which dependency is missing: Read error message
2. Verify dependency app exists: List registered apps
3. Check dependency installation status
Solution:
1. Install missing dependency first
2. Verify dependency is in "installed" state
3. Retry installation of dependent app
Circular Dependency Detected
Problem: RegisterApp fails with "CyclicDependencyError"
Diagnosis:
1. Review error message for cycle path
2. Example: [app-a, app-b, app-c, app-a]
3. Identify unnecessary dependency causing cycle
Solution:
1. Remove one dependency in the cycle
2. Redesign app architecture to eliminate circular reference
3. Consider extracting shared code to separate lib
Version Constraint Not Satisfied (Planned Behavior)
This error does not currently occur because version constraint validation is not yet implemented. This section describes the planned behavior once semantic version enforcement is added.
Problem: InstallApp fails with "Version constraint not satisfied" (when implemented)
Diagnosis:
1. Check required version: app-billing requires app-data ^2.0.0
2. Check installed version: app-data 1.5.0 is installed
3. Version mismatch: 1.5.0 does not satisfy ^2.0.0
Solution:
1. Upgrade dependency: Update app-data to 2.x
2. Or downgrade dependent: Use app-billing version compatible with 1.x
3. Verify version compatibility in manifest
Cannot Uninstall - Has Dependents
Problem: UninstallApp fails with "HasDependentsError"
Diagnosis:
1. Check dependent list in error message
2. Identify which apps depend on this app
3. Check if dependencies are HARD or SOFT
Solution:
1. Uninstall dependent apps first (in reverse order)
2. Or use force flag (dangerous, breaks dependents)
3. Or keep app installed
Monitoring
Dependency Graph Visualization
Platform should provide:
- Visual dependency graph (DAG)
- Highlight cycles (red)
- Show HARD (solid lines) vs SOFT (dashed lines)
- Interactive navigation
Version Compatibility Matrix
App Compatibility Matrix:
| App | Version | Depends On | Compatible |
|-------------|---------|---------------------|------------|
| billing | 1.0.0 | auth ^2.0.0 | ✓ |
| billing | 1.0.0 | data >=1.5.0 | ✓ |
| reporting | 2.0.0 | billing ^1.0.0 | ✓ |
| analytics | 1.0.0 | search >=1.0.0 | ✗ (1.0.0) |
Outdated Dependency Warnings
Platform should warn:
- Dependency has newer version available
- Dependency has security vulnerabilities
- Dependency will be deprecated
- Dependency breaking changes coming
Related Concepts
- Application Lifecycle - Installation and uninstallation flows
- Application Manifest & Registration - Declaring dependencies
- App State Machine & Lifecycle Enforcement - State transitions
- Developer Platform & SDK - SDK dependency helpers
Further Reading
- Semantic Versioning - Version numbering standard
- Graph Theory - Topological Sort - Algorithm details
- Building Apps - Declaring dependencies in practice