The Branch By Abstraction Pattern

The Branch By Abstraction Pattern

An Introduction with NestJS

·

2 min read

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 in his blog as a:

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

Usage

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

image.png

  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 implementation of the legacy service e.g. the new service for the given abstraction
  4. Remove the legacy service and switch to the new implementation
  5. Remove the abstraction

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

Branch By Abstraction is often used withintrunk-based development

Example with NestJS

Give the following project structure:

├── 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')
  }
}

Introduce an abstraction

nest generate interface abstraction
export interface Abstraction {
  reverseInput(input: string): string
}

Use the abstraction interface

...
export class LegacyService implements Abstraction
...

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('')
  }
}

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