Skip to main content

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 DependencyResolver only 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:

  1. All referenced dependency apps exist
  2. No cycles are created
  3. Dependency depth doesn't exceed max (default: 50)
  4. 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
Implementation Status

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 DependencyResolver only 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)

Future Feature

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

Further Reading