Multiple pricing models (percentage discount, flat discount, buy-one-get-one, volume-based tiers) are embedded in a single service with conditional logic. Adding a new pricing strategy requires modifying the existing service, violating the Open/Closed Principle.
The Strategy pattern extracts each pricing algorithm into its own class implementing PricingStrategy. The PricingService holds a reference to the current strategy and delegates calculations. Strategies are interchangeable at runtime.
pricing-strategy.interface.ts percentage-discount.strategy.ts flat-discount.strategy.ts bogo.strategy.ts tiered-pricing.strategy.ts pricing.service.ts
export interface PricingResult {
finalPrice : number ;
discount : number ;
strategy : string ;
}
export interface PricingStrategy {
readonly name : string ;
calculate ( basePrice : number , quantity : number ): PricingResult ;
}
export const PRICING_STRATEGY = Symbol ( 'PRICING_STRATEGY' ); import { Injectable } from '@nestjs/common' ;
import { PricingStrategy , PricingResult } from './pricing-strategy.interface' ;
@ Injectable ()
export class PercentageDiscountStrategy implements PricingStrategy {
readonly name = 'percentage' ;
constructor ( private readonly percentage : number ) {}
calculate ( basePrice : number , quantity : number ): PricingResult {
const subtotal = basePrice * quantity ;
const discount = subtotal * this . percentage / 100 ;
const finalPrice = subtotal - discount ;
return {
finalPrice : Math . round ( finalPrice * 100 ) / 100 ,
discount : Math . round ( discount * 100 ) / 100 ,
strategy : ` ${ this . percentage } % discount` ,
};
}
} import { Injectable } from '@nestjs/common' ;
import { PricingStrategy , PricingResult } from './pricing-strategy.interface' ;
@ Injectable ()
export class FlatDiscountStrategy implements PricingStrategy {
readonly name = 'flat' ;
constructor ( private readonly flatAmount : number ) {}
calculate ( basePrice : number , quantity : number ): PricingResult {
const subtotal = basePrice * quantity ;
const discount = Math . min ( this . flatAmount , subtotal );
const finalPrice = subtotal - discount ;
return {
finalPrice : Math . round ( finalPrice * 100 ) / 100 ,
discount : Math . round ( discount * 100 ) / 100 ,
strategy : `$ ${ this . flatAmount } flat discount` ,
};
}
} import { Injectable } from '@nestjs/common' ;
import { PricingStrategy , PricingResult } from './pricing-strategy.interface' ;
@ Injectable ()
export class BogoStrategy implements PricingStrategy {
readonly name = 'bogo' ;
calculate ( basePrice : number , quantity : number ): PricingResult {
const freeItems = Math . floor ( quantity / 2 );
const paidItems = quantity - freeItems ;
const finalPrice = basePrice * paidItems ;
const discount = basePrice * freeItems ;
return {
finalPrice : Math . round ( finalPrice * 100 ) / 100 ,
discount : Math . round ( discount * 100 ) / 100 ,
strategy : `Buy one get one free ( ${ freeItems } free item ${ freeItems !== 1 ? 's' : '' } )` ,
};
}
} import { Injectable } from '@nestjs/common' ;
import { PricingStrategy , PricingResult } from './pricing-strategy.interface' ;
@ Injectable ()
export class TieredPricingStrategy implements PricingStrategy {
readonly name = 'tiered' ;
calculate ( basePrice : number , quantity : number ): PricingResult {
let discountPercentage : number ;
let tierName : string ;
if ( quantity > 50 ) {
discountPercentage = 20 ;
tierName = '50+ items (20% off)' ;
} else if ( quantity >= 11 ) {
discountPercentage = 10 ;
tierName = '11-50 items (10% off)' ;
} else {
discountPercentage = 0 ;
tierName = '1-10 items (base price)' ;
}
const subtotal = basePrice * quantity ;
const discount = subtotal * discountPercentage / 100 ;
const finalPrice = subtotal - discount ;
return {
finalPrice : Math . round ( finalPrice * 100 ) / 100 ,
discount : Math . round ( discount * 100 ) / 100 ,
strategy : `Tiered pricing: ${ tierName } ` ,
};
}
} import { Inject , Injectable , BadRequestException } from '@nestjs/common' ;
import type { PricingStrategy , PricingResult } from './pricing-strategy.interface' ;
import { PRICING_STRATEGY } from './pricing-strategy.interface' ;
@ Injectable ()
export class PricingService {
private readonly strategies : Map < string , PricingStrategy >;
constructor (
@ Inject ( PRICING_STRATEGY )
private readonly defaultStrategy : PricingStrategy ,
@ Inject ( 'ALL_PRICING_STRATEGIES' )
strategies : PricingStrategy [],
) {
this . strategies = new Map ( strategies . map (( s ) => [ s . name , s ]));
}
getDefaultStrategyName (): string {
return this . defaultStrategy . name ;
}
getAvailableStrategies (): string [] {
return [... this . strategies . keys ()];
}
calculatePrice ( basePrice : number , quantity : number , strategyName ? : string ): PricingResult {
const strategy = strategyName
? this . resolveStrategy ( strategyName )
: this . defaultStrategy ;
return strategy . calculate ( basePrice , quantity );
}
private resolveStrategy ( name : string ): PricingStrategy {
const strategy = this . strategies . get ( name );
if ( ! strategy ) {
throw new BadRequestException (
`Unknown strategy " ${ name } ". Available: ${ this . getAvailableStrategies (). join ( ', ' ) } ` ,
);
}
return strategy ;
}
}
strategy.controller.ts strategy.service.ts strategy.module.ts
import { Controller , Get , Post , Body , Query } from '@nestjs/common' ;
import { PricingCalculationService } from './integration.service' ;
@ Controller ( 'pricing' )
export class PricingController {
constructor ( private readonly pricingCalculation : PricingCalculationService ) {}
@ Get ( 'strategies' )
getStrategies () {
return this . pricingCalculation . getStrategies ();
}
@ Post ( 'calculate' )
calculate (
@ Body () body : { basePrice : number ; quantity : number },
@ Query ( 'strategy' ) strategy ? : string ,
) {
return this . pricingCalculation . calculate (
body . basePrice ,
body . quantity ,
strategy ,
);
}
} import { Injectable } from '@nestjs/common' ;
import { PricingService } from './pricing.service' ;
@ Injectable ()
export class PricingCalculationService {
constructor ( private readonly pricingService : PricingService ) {}
getStrategies () {
return this . pricingService . getAvailableStrategies ();
}
calculate ( basePrice : number , quantity : number , strategy ? : string ) {
return this . pricingService . calculatePrice ( basePrice , quantity , strategy );
}
} import { Module } from '@nestjs/common' ;
import type { PricingStrategy } from './pricing-strategy.interface' ;
import { PRICING_STRATEGY } from './pricing-strategy.interface' ;
import { PricingController } from './integration.controller' ;
import { PricingCalculationService } from './integration.service' ;
import { PricingService } from './pricing.service' ;
import { PercentageDiscountStrategy } from './percentage-discount.strategy' ;
import { FlatDiscountStrategy } from './flat-discount.strategy' ;
import { BogoStrategy } from './bogo.strategy' ;
import { TieredPricingStrategy } from './tiered-pricing.strategy' ;
@ Module ({
controllers : [ PricingController ],
providers : [
PercentageDiscountStrategy ,
FlatDiscountStrategy ,
BogoStrategy ,
TieredPricingStrategy ,
{
provide : PRICING_STRATEGY ,
useExisting : PercentageDiscountStrategy ,
},
{
provide : 'ALL_PRICING_STRATEGIES' ,
useFactory : (... strategies : PricingStrategy []) => strategies ,
inject : [
PercentageDiscountStrategy ,
FlatDiscountStrategy ,
BogoStrategy ,
TieredPricingStrategy ,
],
},
PricingCalculationService ,
PricingService ,
],
exports : [ PricingService ],
})
export class StrategyModule {}