Back to Software Engineering & Delivery Mastery Series CI/CD Platform Deep Dive

CI/CD Platform Deep Dive: CircleCI

May 14, 2026 Wasil Zafar 40 min read

Master CircleCI — config.yml syntax, orbs for reusable configuration, workflows with fan-out/fan-in, Docker layer caching, resource classes, test splitting for parallel execution, and optimization strategies for fast, cost-effective pipelines.

Table of Contents

  1. Introduction
  2. Configuration Basics
  3. Executors
  4. Orbs
  5. Workflows
  6. Docker Layer Caching
  7. Test Splitting & Parallelism
  8. Caching
  9. Contexts & Secrets
  10. Pipeline Parameters
  11. Self-Hosted Runners
  12. Insights & Optimization
  13. Exercises

Introduction

CircleCI is a cloud-native CI/CD platform that has been a mainstay of the developer toolchain since 2011. Built from the ground up for speed and developer experience, CircleCI differentiated itself through Docker-first execution, generous parallelism options, and a powerful caching system that can cut build times by 50-80%.

While GitHub Actions has captured the "CI adjacent to code" market, CircleCI remains popular for teams that need advanced optimization features — particularly Docker Layer Caching, intelligent test splitting, and granular resource class selection. Organizations with large test suites and complex Docker builds often find CircleCI's purpose-built features deliver significantly faster pipelines.

Key Insight: CircleCI's competitive advantage is speed optimization. Features like Docker Layer Caching (DLC), time-based test splitting, and large resource classes exist specifically to minimize pipeline duration. If your primary pain point is slow CI, CircleCI likely has a solution.

Market Positioning

CircleCI occupies the "premium CI" space — more expensive than GitHub Actions' free tier but offering performance features that justify the cost for engineering-heavy organizations. Its credit-based pricing model charges for actual compute usage rather than flat per-user fees, making it economical for teams with sporadic build patterns and expensive for teams running constant heavy workloads.

Configuration Basics

CircleCI configuration lives in .circleci/config.yml at the repository root. Version 2.1 (current) supports orbs, reusable executors, parameterized jobs, and pipeline parameters.

# .circleci/config.yml
version: 2.1

orbs:
  node: circleci/node@5.2.0
  docker: circleci/docker@2.6.0

executors:
  app-executor:
    docker:
      - image: cimg/node:20.11
      - image: cimg/postgres:16.1
        environment:
          POSTGRES_DB: testdb
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
    resource_class: medium
    working_directory: ~/project

jobs:
  install-deps:
    executor: app-executor
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
          cache-path: node_modules
      - persist_to_workspace:
          root: ~/project
          paths:
            - node_modules

  lint:
    executor: app-executor
    steps:
      - checkout
      - attach_workspace:
          at: ~/project
      - run:
          name: Run ESLint
          command: npm run lint

  test:
    executor: app-executor
    parallelism: 4
    steps:
      - checkout
      - attach_workspace:
          at: ~/project
      - run:
          name: Run tests with splitting
          command: |
            TESTFILES=$(circleci tests glob "tests/**/*.test.js" | circleci tests split --split-by=timings)
            npm test -- $TESTFILES
      - store_test_results:
          path: test-results
      - store_artifacts:
          path: coverage

  build-image:
    executor: docker/docker
    steps:
      - setup_remote_docker:
          docker_layer_caching: true
      - checkout
      - docker/build:
          image: myorg/myapp
          tag: ${CIRCLE_SHA1}
      - docker/push:
          image: myorg/myapp
          tag: ${CIRCLE_SHA1}

workflows:
  build-test-deploy:
    jobs:
      - install-deps
      - lint:
          requires: [install-deps]
      - test:
          requires: [install-deps]
      - build-image:
          requires: [lint, test]
          filters:
            branches:
              only: main

Key concepts in the configuration:

  • Orbs — Reusable configuration packages (like GitHub Actions marketplace actions)
  • Executors — Named execution environments with Docker images, resource classes, and environment variables
  • Jobs — Collections of steps that run on a single executor
  • Workflows — Orchestrate jobs with dependencies, filters, and scheduling
  • Workspaces — Persist and share data between jobs in a workflow

Executors

CircleCI offers four executor types, each optimized for different workloads. Choosing the right executor significantly impacts both speed and cost.

Executor Types

Docker: Most common. Runs steps inside a Docker container. Supports multiple service containers (databases, caches). Fast startup (~5s). Cannot run Docker-in-Docker without setup_remote_docker.

Machine: Full Linux VM. Required for Docker builds without DLC, privileged operations, and full networking control. Slower startup (~30-60s). Supports Docker natively.

macOS: macOS VMs for iOS/macOS builds. Xcode pre-installed. Most expensive resource class. Required for Apple-specific toolchains.

Windows: Windows Server VMs. Visual Studio Build Tools, .NET Framework pre-installed. Required for Windows-native builds.

# Docker executor with service containers
executors:
  integration-test:
    docker:
      - image: cimg/python:3.12  # Primary container
      - image: cimg/redis:7.2    # Service container
      - image: cimg/postgres:16.1
        environment:
          POSTGRES_DB: app_test
          POSTGRES_USER: test
          POSTGRES_PASSWORD: secret
    resource_class: large  # 4 vCPU, 8GB RAM
    environment:
      DATABASE_URL: postgresql://test:secret@localhost:5432/app_test
      REDIS_URL: redis://localhost:6379

# Machine executor for Docker builds
executors:
  docker-builder:
    machine:
      image: ubuntu-2404:current
    resource_class: medium  # 2 vCPU, 7.5GB RAM

# ARM executor
executors:
  arm-builder:
    machine:
      image: ubuntu-2404:current
    resource_class: arm.medium  # ARM-based 2 vCPU

Resource classes determine CPU and memory allocation. CircleCI charges credits per minute based on the resource class:

Resource ClassvCPURAMCredits/min
small12 GB5
medium24 GB10
medium+36 GB15
large48 GB20
xlarge816 GB40
2xlarge1632 GB80
2xlarge+2040 GB100

Orbs

Orbs are CircleCI's reusable configuration packages — shareable bundles of jobs, commands, and executors. They're the equivalent of GitHub Actions marketplace actions but with deeper integration into CircleCI's config system.

# Using community orbs
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@4.1.3
  slack: circleci/slack@4.13.3
  docker: circleci/docker@2.6.0
  node: circleci/node@5.2.0
  terraform: circleci/terraform@3.3.0

jobs:
  deploy:
    executor: aws-cli/default
    steps:
      - checkout
      - aws-cli/setup:
          role_arn: arn:aws:iam::123456789:role/ci-deploy
      - run:
          name: Deploy to ECS
          command: |
            aws ecs update-service \
              --cluster production \
              --service my-app \
              --force-new-deployment
      - slack/notify:
          event: pass
          template: basic_success_1
# Creating a custom orb (orb.yml)
version: 2.1

description: |
  Custom orb for deploying to our Kubernetes clusters.

executors:
  k8s-deployer:
    docker:
      - image: bitnami/kubectl:1.29

commands:
  deploy:
    parameters:
      cluster:
        type: string
      namespace:
        type: string
        default: default
      manifest:
        type: string
    steps:
      - run:
          name: Configure kubectl
          command: |
            echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
      - run:
          name: Apply manifests
          command: |
            kubectl apply -f << parameters.manifest >> \
              --namespace << parameters.namespace >> \
              --context << parameters.cluster >>
      - run:
          name: Verify rollout
          command: |
            kubectl rollout status deployment/app \
              --namespace << parameters.namespace >> \
              --timeout=300s

jobs:
  deploy-to-cluster:
    executor: k8s-deployer
    parameters:
      cluster:
        type: string
      namespace:
        type: string
    steps:
      - checkout
      - deploy:
          cluster: << parameters.cluster >>
          namespace: << parameters.namespace >>
          manifest: kubernetes/

Workflows

Workflows orchestrate job execution with dependencies, parallel paths, approval gates, and branch/tag filtering. They provide visual pipeline representation in the CircleCI UI.

CircleCI Workflow: Fan-Out/Fan-In Pattern
flowchart TD
    INSTALL[install-deps] --> LINT[lint]
    INSTALL --> UNIT[unit-test]
    INSTALL --> INTEG[integration-test]
    INSTALL --> SEC[security-scan]
    
    LINT --> BUILD[build-image]
    UNIT --> BUILD
    INTEG --> BUILD
    SEC --> BUILD
    
    BUILD --> STAGING[deploy-staging]
    STAGING --> APPROVE{Manual Approval}
    APPROVE --> PROD[deploy-production]
    
    style INSTALL fill:#3B9797,color:#fff
    style BUILD fill:#132440,color:#fff
    style APPROVE fill:#BF092F,color:#fff
    style PROD fill:#132440,color:#fff
                            
# Complex workflow with fan-out/fan-in and approval
workflows:
  build-test-deploy:
    jobs:
      # Fan-out: parallel jobs after install
      - install-deps
      - lint:
          requires: [install-deps]
      - unit-test:
          requires: [install-deps]
      - integration-test:
          requires: [install-deps]
      - security-scan:
          requires: [install-deps]
      
      # Fan-in: build requires all checks
      - build-image:
          requires: [lint, unit-test, integration-test, security-scan]
          filters:
            branches:
              only: [main, release/*]
      
      # Deploy staging automatically
      - deploy-staging:
          requires: [build-image]
          context: staging-context
      
      # Manual approval gate
      - approve-production:
          type: approval
          requires: [deploy-staging]
      
      # Deploy production after approval
      - deploy-production:
          requires: [approve-production]
          context: production-context

  # Scheduled workflow (nightly builds)
  nightly-full-test:
    triggers:
      - schedule:
          cron: "0 2 * * *"
          filters:
            branches:
              only: main
    jobs:
      - full-integration-suite
      - performance-tests
      - security-audit

Docker Layer Caching

Docker Layer Caching (DLC) is CircleCI's premium feature that persists Docker build layers between pipeline runs. For Dockerfiles that haven't changed in early layers (OS packages, dependencies), DLC can reduce build times from 10+ minutes to under 1 minute.

# Enable DLC with setup_remote_docker
jobs:
  build-docker:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true  # Enables DLC (costs 200 credits/run)
      - run:
          name: Build Docker image
          command: |
            docker build -t myapp:${CIRCLE_SHA1} .
            docker push myregistry/myapp:${CIRCLE_SHA1}

  # Machine executor with DLC
  build-docker-machine:
    machine:
      image: ubuntu-2404:current
      docker_layer_caching: true  # DLC on machine executor
    steps:
      - checkout
      - run:
          name: Build multi-arch image
          command: |
            docker buildx create --use
            docker buildx build \
              --platform linux/amd64,linux/arm64 \
              --tag myregistry/myapp:${CIRCLE_SHA1} \
              --push .
Cost Consideration: DLC costs 200 credits per job run regardless of whether the cache is hit. For images that change frequently (invalidating caches), DLC may cost more than it saves. Analyze your cache hit rate in CircleCI Insights before enabling DLC broadly.

Test Splitting & Parallelism

CircleCI's test splitting distributes test files across parallel containers to minimize total test time. The circleci tests split command supports three strategies: by filename, by filesize, and by timing data (most effective).

# Test splitting with parallelism
jobs:
  test:
    docker:
      - image: cimg/python:3.12
    parallelism: 8  # Run 8 parallel containers
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: pip install -r requirements.txt
      - run:
          name: Run tests with time-based splitting
          command: |
            # Glob test files
            TESTFILES=$(circleci tests glob "tests/**/test_*.py" | \
              circleci tests split --split-by=timings)
            
            # Run only this container's portion
            pytest $TESTFILES \
              --junitxml=test-results/junit.xml \
              --cov=src \
              --cov-report=xml
      - store_test_results:
          path: test-results  # Feeds timing data back to CircleCI
      - store_artifacts:
          path: coverage
# JavaScript test splitting with Jest
jobs:
  test-js:
    docker:
      - image: cimg/node:20.11
    parallelism: 4
    steps:
      - checkout
      - run: npm ci
      - run:
          name: Run Jest with splitting
          command: |
            TESTFILES=$(circleci tests glob "src/**/*.test.{js,ts}" | \
              circleci tests split --split-by=timings)
            npx jest $TESTFILES \
              --ci \
              --reporters=default \
              --reporters=jest-junit
          environment:
            JEST_JUNIT_OUTPUT_DIR: test-results
      - store_test_results:
          path: test-results
Timing Data: The first run uses filename-based splitting. After store_test_results uploads JUnit XML data, subsequent runs use actual test durations for optimal distribution. Timing data is retained for 30 days.

Caching

CircleCI's caching system persists files between pipeline runs — primarily for dependency installation (node_modules, pip packages, Maven artifacts). Effective caching can reduce a 3-minute npm install to a 5-second cache restore.

# Dependency caching with fallback keys
jobs:
  build:
    docker:
      - image: cimg/node:20.11
    steps:
      - checkout
      - restore_cache:
          keys:
            # Exact match (lockfile unchanged)
            - v2-deps-{{ checksum "package-lock.json" }}
            # Partial match (some deps changed)
            - v2-deps-
      - run: npm ci
      - save_cache:
          key: v2-deps-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
            - ~/.npm
# Multi-language caching
jobs:
  build-monorepo:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      # Python dependencies
      - restore_cache:
          keys:
            - v1-pip-{{ checksum "backend/requirements.txt" }}
            - v1-pip-
      - run: pip install -r backend/requirements.txt
      - save_cache:
          key: v1-pip-{{ checksum "backend/requirements.txt" }}
          paths:
            - ~/.cache/pip
      
      # Go modules
      - restore_cache:
          keys:
            - v1-gomod-{{ checksum "services/go.sum" }}
            - v1-gomod-
      - run: cd services && go mod download
      - save_cache:
          key: v1-gomod-{{ checksum "services/go.sum" }}
          paths:
            - ~/go/pkg/mod

Contexts & Secrets

Contexts provide organization-level secret management. Unlike project-level environment variables, contexts can be shared across multiple projects and restricted to specific security groups.

# Using contexts in workflows
workflows:
  deploy-pipeline:
    jobs:
      - build
      - deploy-staging:
          requires: [build]
          context:
            - aws-staging      # AWS credentials for staging
            - slack-notify     # Slack webhook URL
      - deploy-production:
          requires: [deploy-staging]
          context:
            - aws-production   # Different AWS credentials for production
            - slack-notify
          filters:
            branches:
              only: main

Context security features include: restricting context access to specific GitHub teams/groups, requiring approval to use a context, and audit logging of all context usage. Environment variables within a context are masked in build output automatically.

Pipeline Parameters

Pipeline parameters enable dynamic configuration — conditional workflows based on parameters, triggered via API or UI. Combined with setup workflows, they enable monorepo path filtering and dynamic pipeline generation.

# Pipeline parameters for conditional workflows
version: 2.1

parameters:
  run-integration-tests:
    type: boolean
    default: false
  deploy-env:
    type: enum
    enum: [staging, production]
    default: staging
  service:
    type: string
    default: ""

jobs:
  deploy:
    docker:
      - image: cimg/base:current
    steps:
      - run:
          name: Deploy to << pipeline.parameters.deploy-env >>
          command: |
            echo "Deploying << pipeline.parameters.service >> to << pipeline.parameters.deploy-env >>"

workflows:
  standard:
    when:
      not: << pipeline.parameters.run-integration-tests >>
    jobs:
      - build
      - test
      - deploy:
          requires: [test]
          filters:
            branches:
              only: main

  integration:
    when: << pipeline.parameters.run-integration-tests >>
    jobs:
      - full-integration-suite
# Trigger pipeline with parameters via API
curl -X POST https://circleci.com/api/v2/project/gh/myorg/myrepo/pipeline \
  -H "Circle-Token: $CIRCLECI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "branch": "main",
    "parameters": {
      "run-integration-tests": true,
      "deploy-env": "staging",
      "service": "backend"
    }
  }'

Self-Hosted Runners

CircleCI runners let you execute jobs on your own infrastructure — required for accessing private networks, specialized hardware (GPUs), or compliance-restricted environments. Runners support Linux, macOS, Windows, and container-based execution.

# Using self-hosted runner
jobs:
  deploy-internal:
    machine: true
    resource_class: myorg/private-network-runner  # Self-hosted runner class
    steps:
      - checkout
      - run:
          name: Deploy to internal Kubernetes
          command: |
            kubectl apply -f manifests/ --context internal-cluster
      - run:
          name: Run smoke tests against internal services
          command: |
            curl -f http://internal-app.corp.local/health
# Install CircleCI runner on Linux
# 1. Create resource class in CircleCI UI (Organization Settings > Self-Hosted Runners)
# 2. Install runner agent on your machine

curl -s https://raw.githubusercontent.com/CircleCI-Public/runner-installation-files/main/download-launch-agent.sh | bash

# Configure runner
cat > /etc/circleci-runner/circleci-runner-config.yaml <<EOF
api:
  auth_token: YOUR_RUNNER_TOKEN
runner:
  name: my-runner-01
  working_directory: /var/lib/circleci-runner/workdir
  cleanup_working_directory: true
EOF

# Start runner service
systemctl enable circleci-runner
systemctl start circleci-runner

Insights & Optimization

CircleCI Insights provides analytics on pipeline performance — helping identify slow jobs, flaky tests, and credit usage patterns. Key optimization strategies:

CircleCI Optimization Decision Tree
flowchart TD
    START[Slow Pipeline?] --> Q1{Where is time spent?}
    Q1 -->|Dependencies| C1[Optimize caching]
    Q1 -->|Tests| C2[Enable parallelism + splitting]
    Q1 -->|Docker builds| C3[Enable DLC]
    Q1 -->|Waiting| C4[Reduce job dependencies]
    
    C1 --> R1[Use checksum keys + fallbacks]
    C2 --> R2[Set parallelism: 4-8 + timings split]
    C3 --> R3[Analyze cache hit rate first]
    C4 --> R4[Fan-out independent jobs]
    
    style START fill:#BF092F,color:#fff
    style C1 fill:#3B9797,color:#fff
    style C2 fill:#3B9797,color:#fff
    style C3 fill:#3B9797,color:#fff
    style C4 fill:#3B9797,color:#fff
                            
  • Right-size resource classes — Don't use xlarge for jobs that only need small. Monitor CPU/memory utilization in Insights.
  • Parallelize aggressively — Test splitting with 4-8 containers often provides the best cost/speed tradeoff.
  • Workspace vs cache — Use workspaces for passing data within a single workflow run. Use caches for data that persists across runs (dependencies).
  • Filter early — Use path filtering to skip jobs that don't need to run. Fail fast with lint before expensive tests.
  • Scheduled workflows — Run expensive operations (security scans, full integration suites) on schedule rather than every commit.

Exercises

Exercise 1: Optimized Node.js Pipeline

Create a CircleCI config for a Node.js application with: npm dependency caching (with checksum-based keys and fallback), ESLint in a separate job, Jest tests split across 4 parallel containers with timing-based splitting, and Docker image build with DLC. The full pipeline should complete in under 3 minutes for a 500-test suite.

Exercise 2: Custom Orb Development

Create a custom orb that encapsulates your team's deployment process. Include: a reusable executor, parameterized commands for building and deploying, and a complete job that teams can use with minimal configuration. Publish the orb to your organization's namespace and consume it from a project config.

Exercise 3: Monorepo Dynamic Config

Implement a setup workflow for a monorepo with 3 services. The setup workflow should detect which directories changed (using git diff), set pipeline parameters accordingly, and trigger only the relevant service pipelines. Verify that changing backend/ only runs backend jobs.

Exercise 4: Cost Optimization Analysis

Take an existing CircleCI pipeline and optimize it for credit efficiency. Analyze: which jobs use oversized resource classes, where caching could reduce install times, whether DLC cache hit rate justifies its cost, and where parallelism provides diminishing returns. Target a 40% credit reduction while maintaining or improving total pipeline duration.