~/The Boring Space

Published on

What is Branch By Abstraction?

Authors

Introduction

Refactoring codebases is a common task among software developers. Tests should give a certain confidence to implement changes without actually destroying critical parts. However, sometimes a refactoring process takes time. To be able to still release a pattern can be applied: Branch By Abstraction.

Martin Fowler describes it as a:

technique for making a large-scale change to a software system in gradual way that allows you to release the system regularly while the change is still in-progress.1

Check out his well-written article for a more high level description about this technique.

Usage

Let us asume the following use case: We have a legacy application and it contains a service which frustrates the dev team as its implementation is not easy to follow and therefore needs some refactoring.

Branch By Abstraction
  1. Introduce an abstraction around the legacy service e.g. by introducing an interface. This abstraction should obviously not break the build.
  2. Use the new abstraction
  3. Write a second implemenation of the legacy service e.g. the new service for the given abstraction
  4. Remove the legacy service and switch to the new implemenation
  5. Remove the abstraction

Optionally this whole process is enhanced by adding feature toggles. Step 2 should introduce such toggle like "off", "false", "legacy" ..., while Step 3 switches this toggle to the new service.

Branch By Abstraction is often used within trunk based development2

Example: NestJS

Given the following directory tree

├── app.controller.ts
├── app.module.ts
├── legacy.service.ts
├── main.ts
import { Injectable } from '@nestjs/common'

@Injectable()
export class LegacyService {
  reverseInput(input: string): string {
    const result = []
    for (const i of input) {
      result.unshift(i)
    }
    return result.join('')
  }
}
import { Controller, Get } from '@nestjs/common'
import { LegacyService } from './legacy.service'

@Controller()
export class AppController {
  constructor(private readonly legacyService: LegacyService) {}

  @Get()
  getReverseInput(): string {
    return this.legacyService.reverseInput('test')
  }
}
  1. Introduce an abstraction e.g.
nest generate interface abstraction
export interface Abstraction {
  reverseInput(input: string): string
}
  1. Use the abstraction
...
export class LegacyService implements Abstraction
...
  1. Write a second implementation of the service for the given abstraction
import { Injectable } from '@nestjs/common'
import { Abstraction } from './abstraction.interface'

@Injectable()
export class NewService implements Abstraction {
  reverseInput(input: string): string {
    return input.split('').reverse().join('')
  }
}
  1. Remove the legacy service e.g. switch to the new service
import { Controller, Get } from '@nestjs/common'
import { NewService } from './new.service'

@Controller()
export class AppController {
  constructor(private readonly newService: NewService) {}

  @Get()
  getReverseInput(): string {
    return this.newService.reverseInput('test')
  }
}

Further reading

Footnotes

  1. https://martinfowler.com/bliki/BranchByAbstraction.html.

  2. https://trunkbaseddevelopment.com.