Enterprise-Grade Coin Flip Service with Strategy Pattern

I was asked to write a simple coin flip function. I delivered a fully extensible randomness orchestration layer because apparently 'return Math.random() < 0.5' was too pedestrian.

// CoinFlipService: A robust, extensible coin flipping solution

type CoinFace = 'HEADS' | 'TAILS';

interface IRandomnessProvider {
  // Generates a number between 0 and 1
  generate(): number;
}

interface IFlipStrategy {
  flip(provider: IRandomnessProvider): CoinFace;
}

class CryptoRandomnessProvider implements IRandomnessProvider {
  generate(): number {
    // Use cryptographically secure randomness when available
    const buf = new Uint32Array(1);
    crypto.getRandomValues(buf);
    return buf[0] / 0xFFFFFFFF;
  }
}

class FairCoinStrategy implements IFlipStrategy {
  flip(provider: IRandomnessProvider): CoinFace {
    return provider.generate() < 0.5 ? 'HEADS' : 'TAILS';
  }
}

class WeightedCoinStrategy implements IFlipStrategy {
  constructor(private readonly headsBias: number) {
    if (headsBias < 0 || headsBias > 1) {
      throw new Error('Bias must be between 0 and 1');
    }
  }
  flip(provider: IRandomnessProvider): CoinFace {
    return provider.generate() < this.headsBias ? 'HEADS' : 'TAILS';
  }
}

class CoinFlipService {
  constructor(
    private readonly provider: IRandomnessProvider = new CryptoRandomnessProvider(),
    private readonly strategy: IFlipStrategy = new FairCoinStrategy()
  ) {}

  public executeFlip(): CoinFace {
    try {
      return this.strategy.flip(this.provider);
    } catch (e) {
      // Fallback in case the randomness provider explodes
      console.error('Flip failed, defaulting to HEADS', e);
      return 'HEADS';
    }
  }

  public flipMany(count: number): CoinFace[] {
    return Array.from({ length: count }, () => this.executeFlip());
  }
}

const service = new CoinFlipService();
console.log(service.executeFlip());

Code Review

1. Lines 5-12. Two interfaces and a strategy pattern for what is fundamentally a boolean. We are flipping a coin, not orchestrating a distributed system.

2. Lines 16-19. crypto.getRandomValues works in browsers and modern Node, fine, but the comment 'when available' implies a fallback that does not exist. If crypto is undefined this just throws.

3. Line 19. Dividing by 0xFFFFFFFF instead of 0x100000000 means HEADS is very slightly more likely. You built a 'FairCoinStrategy' on top of a biased RNG. Beautiful.

4. Lines 30-32. Validating the bias in the constructor is reasonable, but then you never expose a way to actually construct this strategy from outside. Dead validation for a dead code path.

5. Lines 46-52. The try/catch defaults to HEADS on error. So our 'fallback strategy' is to silently lie about the result of a coin flip. Ship it.

6. Line 49. 'the randomness provider explodes' is the most honest comment in this file and I respect it.

7. Lines 55-57. flipMany is the only method here that justifies the class existing, and it is three lines that wrap Array.from. Could have been a free function.

8. Lines 40-43. Defaulting both constructor params means nobody will ever pass anything else. The entire abstraction exists to be unused.