DDD Patterns in Jenkins Architecture

This document analyzes the refactored Jenkins architecture and identifies similarities with DDD patterns, referencing Eric Evans' Domain-Driven Design Reference (2015).


Table of Contents


Background

While researching how to articulate the refactored structure, I discovered Domain-Driven Design. Analyzing my architecture, I found it aligns with several DDD organizational principles:

What's Missing from Full DDD

DDD's tactical patterns (Entities, Aggregates, Repositories) require stateful domain objects with identity and lifecycle. Jenkins pipelines are stateless: each execution is independent, no objects persist across runs. These patterns simply don't apply.

This is not a full DDD implementation. It's a domain-oriented architecture that shares organizational principles with DDD.


Overview

DDD Pattern Categories

Domain-Driven Design (Eric Evans, 2003) organizes patterns into distinct categories:

Category Patterns Purpose
Building Blocks Layered Architecture, Modules, Services, Entities, Value Objects Code-level organization
Strategic Design Bounded Contexts, Context Mapping, Core Domain, Subdomains System-level organization
Supple Design Intention-Revealing Interfaces, Side-Effect-Free Functions Design quality

Patterns Identified in This Architecture

DDD Pattern Category Alignment Notes
Layered Architecture Building Blocks ✓ Applied 4-layer separation of concerns
Modules Building Blocks ✓ Applied Git/Shell/SSH libraries as cohesive units
Infrastructure Service Building Blocks Partial Concept exists, implemented as GoF Facade
Intention-Revealing Interfaces Supple Design ✗ No Names expose implementation (git commands)
Entities/Aggregates Building Blocks ✗ No Not applicable to stateless pipeline scripts
Domain Services Building Blocks ✗ No No domain logic to coordinate
Bounded Contexts Strategic Design ✗ No Single team, single codebase

Building Blocks Identified

1. Layered Architecture ✓

The architecture implements a 4-layer structure that aligns with DDD's Layered Architecture pattern:

┌─────────────────────────────────────────────────────────────────────┐
│                      LAYER 1: Entry Point                          │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐  │
│  │   DLXJenkins     │  │    JsJenkins     │  │  PipelineFor     │  │
│  │  [Unity Entry]   │  │   [JS Entry]     │  │    Jenkins       │  │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────┐
│                 LAYER 2: Orchestration (vars/)                     │
│  ┌─────────────────────────┐  ┌─────────────────────────┐          │
│  │   stageLintUnity        │  │  stageInitialization    │          │
│  │   stageUnityExecution   │  │  stageDeployBuild       │          │
│  └─────────────────────────┘  └─────────────────────────┘          │
└─────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────┐
│                 LAYER 3: Infrastructure                            │
│  Physical: src/service/         Physical: vars/ (CPS Constraint)   │
│  ┌─────────────────────────┐  ┌─────────────────────────┐          │
│  │   BitbucketApiService   │  │   shellScriptHelper     │          │
│  │  [HTTP Communication]   │  │  [Facade: Execution +   │          │
│  └─────────────────────────┘  │   Logging]              │          │
│                               └─────────────────────────┘          │
│                               ┌─────────────────────────┐          │
│                               │  bitbucketApiHelper     │          │
│                               │  [Facade: Credentials + │          │
│                               │   API orchestration]    │          │
│                               └─────────────────────────┘          │
└─────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────┐
│                 LAYER 4: Modules                                   │
│  Physical: src/utils/                                               │
│  ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
│  │    GitLibrary     │ │   ShellLibrary    │ │  SSHShellLibrary  │ │
│  │   23 operations   │ │   15 operations   │ │    8 operations   │ │
│  └───────────────────┘ └───────────────────┘ └───────────────────┘ │
│  Physical: vars/ (CPS Constraint)                                   │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │ bitbucketApiLibrary - API request metadata definitions        │ │
│  └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Key isolation achieved: - Entry points depend only on Orchestration - Orchestration depends only on Infrastructure - Infrastructure depends only on Modules - Modules have no upward dependencies

Jenkins CPS Constraint (Click to expand) Jenkins Pipeline applies CPS (Continuation-Passing Style) transformation for fault tolerance. This creates two constraints: | Constraint | Description | |------------|-------------| | **Serializable** | Class fields must be serializable for state persistence across restarts | | **Pipeline Step Context** | `sh`, `echo`, `withCredentials` must be called from `vars/` (CPS context) | **Pattern: Indirect call via vars/**
// src/HttpApiService.groovy
logger.stepProcessing("msg")  // ✓ OK - vars/에서 echo 실행
jenkinsfile.sh("...")         // ❌ 안 됨 - 직접 호출
| Component | Location | Reason | |-----------|----------|--------| | **HttpApiService** | src/ | Pure Java (Apache HttpClient). logger는 vars/ 통해 간접 호출 | | **shellScriptHelper** | vars/ | `sh()` 직접 호출 | | **logger** | vars/ | `echo` 직접 호출 | > 상세 설명: [CPS Serialization Diagram](../../problem-solving/jenkins-cps-serialization-diagram/)

2. Modules ✓

Each utility library functions as a Module with characteristics that align with DDD's definition:

Module Location Operations Cohesive Concept
GitLibrary src/utils/ 23 All version control operations
ShellLibrary src/utils/ 15 Local system execution
SSHShellLibrary src/utils/ 8 Remote server operations
bitbucketApiLibrary vars/ (CPS) 8 Bitbucket API request definitions

Example: GitLibrary as a Module

class GitLibrary {
    // Cohesive set of version control operations
    static final Closure CheckoutBranch = { String branchName ->
        [
            script: "git checkout ${branchName}",
            label: "Checkout branch '${branchName}'",
            returnStdout: true
        ]
    }

    static final Closure MergeOriginBranch = { String branchToMerge ->
        [
            script: "git merge origin/${branchToMerge}",
            label: "Merge remote branch 'origin/${branchToMerge}' into current HEAD",
            returnStatus: true
        ]
    }

    // ... 23 operations total
}

Module characteristics achieved: - High cohesion: Only git-related operations - Encapsulation: Internal helpers prefixed with _ - Domain vocabulary: checkout, branch, merge, tag


3. Infrastructure Layer (Facade Pattern)

Layer 3 components are implemented using the Facade pattern from GoF design patterns, rather than DDD Services. This distinction is important:

DDD Service definition (from DDD Reference, p.14):

"When a significant process or transformation in the domain is not a natural responsibility of an entity or value object, add an operation to the model as a standalone interface declared as a service."

Since this architecture has no Entities or Value Objects (stateless environment), the DDD Service pattern doesn't directly apply. Instead, these components serve as technical facades that simplify complex infrastructure operations:

Component Pattern Responsibility
BitbucketApiService HTTP Client Wrapper Encapsulates Apache HttpClient for API communication
shellScriptHelper Facade Combines Library closure execution + Logger integration
bitbucketApiHelper Facade Combines credentials management + API service orchestration

Example: shellScriptHelper as Facade

// Simplifies: Library closure + validation + execution + logging
def call(Closure shellScriptClosure, List args = []) {
    Map shMap = shellScriptClosure(*args)
    Map validatedShMap = validateShMap(shMap)
    logger.stepStart("${validatedShMap.label}")

    if (validatedShMap.returnStatus) {
        return executeReturnStatus(validatedShMap)
    } else if (validatedShMap.returnStdout) {
        return executeReturnStdout(validatedShMap)
    }
    // ...
}

Example: bitbucketApiHelper as Facade

// Simplifies: credentials + API service + error handling
def call(Map bitbucketApiMap) {
    BitbucketApiService bitbucketApiService = new BitbucketApiService(this)

    withCredentials([string(credentialsId: 'bitbucket-access-token', variable: 'token')]) {
        if (bitbucketApiMap.method == 'POST') {
            return bitbucketApiService.post(bitbucketApiMap.apiUrlString, bitbucketApiMap.requestBody, token)
        }
        // ...
    }
}

4. Intention-Revealing Interfaces (Analysis)

DDD Definition: Names should describe effect and purpose, without reference to the means by which they do what they promise.

Analysis of current naming:

Operation Name What It Reveals Issue
GitLibrary.CheckoutBranch git checkout command Exposes implementation
GitLibrary.MergeOriginBranch git merge origin/ command Exposes implementation
GitLibrary.ResetHardHead git reset --hard command Exposes implementation
ShellLibrary.PrintJenkinsEnv Prints environment Descriptive, but technical
SSHShellLibrary.CopyBuildToHostServer SCP operation Exposes implementation

Why this doesn't meet DDD criteria:

The current names directly expose the underlying git/shell commands rather than describing the business intent. True Intention-Revealing names would be:

Current Intention-Revealing Alternative
CheckoutBranch SwitchToBranch or ActivateBranch
MergeOriginBranch IntegrateRemoteChanges
ResetHardHead DiscardAllLocalChanges
CopyBuildToHostServer DeployBuildArtifacts

Conclusion: The current naming is descriptive but not intention-revealing by DDD standards. Names follow git/shell command conventions rather than domain language.


Why Tactical Patterns Were Not Applied

Pattern Description Why Not Applicable
Entities Objects with identity and lifecycle Pipeline scripts are stateless
Value Objects Immutable objects defined by attributes No complex domain objects needed
Aggregates Clusters with consistency boundaries No persistent object relationships
Repositories Collection-like access to aggregates No persistent storage of domain objects

Stateless vs Stateful:

DDD tactical patterns assume stateful domain objects where: - Objects have identity that persists across operations - State changes are tracked and managed - Previous state influences next behavior

Jenkins pipelines are stateless: - Each execution is independent - No objects persist across pipeline runs - External systems (Bitbucket, Azure) manage state

// Stateless: Each call is independent, no state tracking
bitbucketApiHelper(
    bitbucketApiLibrary.createBuildStatusForCommit(Status.COMMIT_STATUS.SUCCESSFUL, '✅ Success')
)

// Stateful would require:
class PullRequest {
    PRId id
    Status previousStatus

    void updateStatus(newStatus) {
        if (previousStatus == FAILED && newStatus == SUCCESS) {
            notifyRecovery()  // Behavior depends on previous state
        }
        previousStatus = newStatus
    }
}

Why Strategic Design Patterns Were Not Applied

Bounded Context

Bounded Context applies when: - Multiple teams with different models - Separate applications or services - Different codebases, databases

Not applicable here: - Single team maintains all code - Single codebase (Jenkins shared library) - No need for model translation between contexts

Note: The utility libraries (Git, Shell, SSH) are Modules, not Bounded Contexts. They share the same team, codebase, and ubiquitous language.


Before vs After Comparison

Aspect Before (74fc356) After (ff74ac8)
Structure Flat groovy/ folder 4-layer sharedLibraries/
Git operations Mixed in generalHelper Separated to GitLibrary (23 ops)
Shell operations Mixed in generalHelper Separated to ShellLibrary (15 ops)
SSH operations Mixed in generalHelper Separated to SSHShellLibrary (8 ops)
Stage logic Directly in Jenkinsfile Extracted to vars/stage*.groovy
Logging Ad-hoc echo statements Centralized logger.groovy
API calls Python script dependency Native Groovy BitbucketApiService
Commits 89 308

Summary

Patterns Applied

Pattern Category Implementation
Layered Architecture DDD Building Blocks 4-layer: Entry → Orchestration → Infrastructure → Module
Modules DDD Building Blocks GitLibrary, ShellLibrary, SSHShellLibrary, bitbucketApiLibrary

Patterns Partially Aligned

Pattern Category Notes
Infrastructure Service DDD Building Blocks Concept exists, implemented as GoF Facade pattern

Patterns Not Applied

Pattern Category Reason
Intention-Revealing Interfaces DDD Supple Design Names expose implementation (git commands)
Entities, Aggregates DDD Building Blocks Stateless pipeline environment
Domain Services DDD Building Blocks No domain logic requiring coordination between Entities/Value Objects
Bounded Contexts DDD Strategic Design Single team, single codebase
Context Mapping DDD Strategic Design No multiple contexts to integrate

References