Skip to content

Proposal: Add lifecycle_type column to apps, droplets, and builds #5067

@philippthun

Description

@philippthun

Problem

Lifecycle type (buildpack, docker, cnb) is not stored directly on apps, droplets, or builds. It is derived at runtime by checking for the existence of associated lifecycle data records:

# Current: checks multiple associations
def lifecycle_type
  return 'buildpack' if buildpack_lifecycle_data
  return 'cnb' if cnb_lifecycle_data
  'docker'
end

This causes:

  1. Expensive list queriesGET /v3/apps?lifecycle_type=docker requires excluding apps from both buildpack_lifecycle_data and cnb_lifecycle_data tables via subqueries
  2. Unnecessary eager loading — Presenters load lifecycle data associations just to determine the type string, even when the full lifecycle data is not needed
  3. Implicit semantics — "docker" is the absence of other types, not an explicit value

Proposed Solution

Add a lifecycle_type VARCHAR column (with index) to apps, droplets, and builds tables.

Deployment Strategy (Two Phases, Zero Downtime)

Deploy 1 (draft PR: #5065):

  • Add nullable columns with concurrent index
  • Populate for new records via AppCreate, V2::AppCreate, and before_create hooks on droplets/builds
  • Fallback to association lookup for existing records
  • Model validation ensures only valid lifecycle types (buildpack, cnb, docker)

Deploy 2:

  • Backfill existing records based on lifecycle data tables
  • Make columns non-nullable
  • Remove fallback logic
  • Optimize AppListFetcher: subqueries → simple WHERE clause
  • Optimize presenters: skip eager-loading lifecycle data for type determination

What the Draft PR Demonstrates

  • Migration adds column + concurrent index (Postgres) / regular index (MySQL)
  • AppCreate / V2::AppCreate set lifecycle_type explicitly
  • DropletModel / BuildModel inherit via before_create hook from associated app
  • lifecycle_type method returns column value with fallback for un-migrated records
  • lifecycle_data method uses lifecycle_type to select the right association (avoids checking all associations)
  • Model validation ensures only valid lifecycle types
  • DropletCopy validates lifecycle type match between source and destination
  • Lifecycle classes assign created lifecycle data back to model instance to avoid Sequel caching issues

Impact After Deploy 2

Area Before After
AppListFetcher lifecycle_type filter 3 subqueries on lifecycle data tables 1 simple WHERE lifecycle_type = ?
Presenters (App, Build, Droplet, Process) Eager-load lifecycle data associations to determine type Read column value directly
Lifecycle type checks in controllers Association lookups O(1) column read
Docker type semantics Implicit (absence of other types) Explicit column value

Endpoints That Benefit

Critical: GET /v3/apps with lifecycle_type filter (subquery elimination)

High: All list and detail endpoints for apps, droplets, builds, and processes (presenter eager-load avoidance)

Medium: POST /v3/apps, POST /v3/builds, POST /v3/apps/:guid/actions/start, POST /v3/apps/:guid/actions/restage, POST /v3/spaces/:guid/actions/apply_manifest, POST /v3/droplets/:guid/copy, staging completion (controller-level type checks)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions