Skip to content

ADR-0002: Cascade Deletion & Selective Soft Deletes ​

Accepted Cross-Project

Date: 2026-02-11

Context ​

Both projects had a shared weakness: cascade deletion of child records used query-level ->delete() which bypasses model observers and events. This was identified as an oversight, not a deliberate performance choice.

Additionally, the Kendo requires selective soft deletes on high-value models for audit trail purposes (see Audit Logging) and ISO 27001 compliance.

The Brick Inventory had a specific risk: DeleteStorageOptionAction performed recursive tree deletion without a transaction wrapper.

Decision ​

Model Declares, Action Executes, Tests Verify ​

Each model declares which relationships must be cascade-deleted:

php
class Issue extends Model
{
    /** Relations that MUST be deleted when this model is deleted */
    public static function cascadeRelations(): array
    {
        return ['comments', 'timeLogs', 'branchLinks'];
    }
}

This is a declaration only — no automatic behavior. The delete Action explicitly handles each relationship. Architecture tests verify that every declared relation is handled in the corresponding delete Action.

Selective Soft Deletes (Kendo Only) ​

Soft deletes apply only to high-value models as determined by data classification. Models NOT soft-deleted follow their parent — when a parent is deleted, children are hard-deleted explicitly by the Action in the correct order.

All Deletions Wrapped in Transactions ​

Every delete Action wraps its full operation (child deletion + parent deletion + audit logging) in a database transaction. No partial deletions.

Foreign Keys as Guards ​

Foreign key constraints are safety nets. They throw an error if a parent is deleted without handling children first. ON DELETE CASCADE is explicitly rejected — deletion must be explicit in the Action, not implicit in the database.

Deletion Pattern Examples ​

Kendo — Soft-Deletable Parent (Issue):

DeleteIssueAction:
  1. Open transaction
  2. Hard-delete comments
  3. Hard-delete time logs
  4. Hard-delete branch links
  5. Soft-delete Issue (set deleted_at)
  6. Record audit log
  7. Commit transaction

Brick Inventory — Hard Delete (StorageOption):

DeleteStorageOptionAction:
  1. Open transaction
  2. Recursively delete children (depth-first)
  3. Hard-delete storage option parts
  4. Hard-delete storage option
  5. Commit transaction

Architecture Test Enforcement ​

php
// All cascade relations are handled in delete actions
it('delete actions handle all declared cascade relations', function () {
    $models = getModelsWithCascadeRelations();
    foreach ($models as $model) {
        $actionSource = getDeleteActionSource($model);
        foreach ($model::cascadeRelations() as $relation) {
            expect($actionSource)->toContain($relation);
        }
    }
});

// All HasMany relations are declared in cascadeRelations
it('all HasMany relations are declared in cascadeRelations', function () {
    // Reflect on model, verify all HasMany/HasOne are listed
});

Options Considered ​

OptionVerdictReason
ON DELETE CASCADE at database levelRejectedImplicit behavior — things get deleted without explicit calls. No opportunity to audit or react in PHP.
Model-level SoftDeletes trait with automatic cascadingRejectedImplicit cascading behind the scenes. Difficult to debug.
Action-level explicit handling with model declarations + testsAcceptedActions remain the authority. Models document their children. Tests catch forgotten relationships at CI time.

Consequences ​

Positive ​

  • Explicit control over deletion order and behavior
  • Architecture tests prevent orphaned data at CI time
  • Foreign keys prevent orphaned data at database level
  • Transaction wrapping eliminates partial deletion risk
  • Pattern is consistent across both projects

Negative ​

  • More verbose delete Actions (must handle each relationship explicitly)
  • Two test suites to maintain (cascade relations declared, delete actions complete)
  • Developers must update both the model declaration and the action when adding relationships

Implementation ​

TerritoryStateNotes
kendoPartialPhase 1 (declarations + arch tests, PR #260) and Phase 2 (delete action refactors + migration to RESTRICT, PRs #261, #263) complete. Phase 3 (soft deletes on classified models) blocked on information classification scheme.
brick-inventoryPartialPhase 1 (PR #76) and Phase 2 (PR #77) complete. Phase 3 (soft deletes) blocked on classification.
ublgeniePartialPhase 1 (cascadeRelations on all 9 models + CascadeRelationsTest, PR #25). DeleteUser FK bug fixed. FKs already RESTRICT (no cascades). Phase 3 not applicable until classification scheme.

Architecture documentation for contributors and collaborators.