Coding Round

Implement a minimal DI container with @Injectable and @Inject decorators.

Example

Without reflect-metadata the tokens are given explicitly with @Inject. Singletons are cached, and a circular dependency is detected with a "resolving" set.

type Ctor<T = unknown> = new (...args: any[]) => T;
const deps = new Map<Ctor, Ctor[]>();
const injectables = new Set<Ctor>();

const Injectable = (): ClassDecorator => (t) => { injectables.add(t as unknown as Ctor); };
const Inject = (token: Ctor): ParameterDecorator => (t, _k, i) => {
  const list = deps.get(t as unknown as Ctor) ?? [];
  list[i] = token;
  deps.set(t as unknown as Ctor, list);
};

class Container {
  private singletons = new Map<Ctor, unknown>();
  private resolving = new Set<Ctor>();
  get<T>(cls: Ctor<T>): T {
    if (this.singletons.has(cls)) return this.singletons.get(cls) as T;
    if (!injectables.has(cls)) throw new Error(cls.name + ' is not a provider');
    if (this.resolving.has(cls)) throw new Error('Circular dependency: ' + [...this.resolving, cls].map((c) => c.name).join(' -> '));
    this.resolving.add(cls);
    const instance = new cls(...(deps.get(cls) ?? []).map((d) => this.get(d)));
    this.resolving.delete(cls);
    this.singletons.set(cls, instance);
    return instance;
  }
}

@Injectable() class Config { gst = 0.18; }
@Injectable() class Pricing {
  constructor(@Inject(Config) private config: Config) {}
  final(price: number) { return Math.round(price * (1 + this.config.gst)); }
}
@Injectable() class Cart {
  constructor(@Inject(Pricing) private pricing: Pricing, @Inject(Config) readonly config: Config) {}
  total(prices: number[]) { return prices.reduce((s, p) => s + this.pricing.final(p), 0); }
}

const c = new Container();
console.log('cart total:', c.get(Cart).total([1299, 999]));
console.log('Config is a singleton:', c.get(Cart).config === c.get(Config));

@Injectable() class OrdersService {}
@Injectable() class PaymentsService {}
deps.set(OrdersService, [PaymentsService]);      // wired by hand: each needs the other
deps.set(PaymentsService, [OrdersService]);
try { c.get(OrdersService); } catch (e) { console.log((e as Error).message); }

Output

cart total: 2712
Config is a singleton: true
Circular dependency: OrdersService -> PaymentsService -> OrdersService

Write a guard and pipe chain: authenticate, check roles, then validate and transform the id parameter.

Example

Guards run first and can stop the request with 401/403; then pipes transform the argument; only then does the handler run.

class HttpError extends Error { constructor(readonly status: number, msg: string) { super(msg); } }
type User = { id: number; roles: string[] };
type Ctx = { headers: Record<string, string>; params: Record<string, string>; user?: User; requiredRoles?: string[] };
type Guard = (ctx: Ctx) => boolean;
type Pipe<I, O> = (value: I) => O;

const tokens: Record<string, User> = { 'tok-asha': { id: 12, roles: ['customer'] }, 'tok-admin': { id: 1, roles: ['admin'] } };

const jwtGuard: Guard = (ctx) => {
  const user = tokens[(ctx.headers.authorization ?? '').replace('Bearer ', '')];
  if (!user) throw new HttpError(401, 'Unauthorized');
  ctx.user = user;
  return true;
};
const rolesGuard: Guard = (ctx) => !ctx.requiredRoles || ctx.requiredRoles.some((r) => ctx.user!.roles.includes(r));
const parseIntPipe: Pipe<string, number> = (v) => {
  if (!/^\d+$/.test(v)) throw new HttpError(400, 'Validation failed (numeric string is expected)');
  return Number(v);
};
const positivePipe: Pipe<number, number> = (v) => { if (v < 1) throw new HttpError(400, 'id must be positive'); return v; };

function handle(ctx: Ctx, handler: (id: number) => string): string {
  try {
    for (const g of [jwtGuard, rolesGuard]) if (!g(ctx)) throw new HttpError(403, 'Forbidden resource');
    const id = positivePipe(parseIntPipe(ctx.params.id));
    return '200 ' + handler(id);
  } catch (e) {
    const err = e as HttpError;
    return err.status + ' ' + err.message;
  }
}

const deleteProduct = (id: number) => 'deleted product ' + id;
const admin = ['admin'];
console.log(handle({ headers: {}, params: { id: '7' }, requiredRoles: admin }, deleteProduct));
console.log(handle({ headers: { authorization: 'Bearer tok-asha' }, params: { id: '7' }, requiredRoles: admin }, deleteProduct));
console.log(handle({ headers: { authorization: 'Bearer tok-admin' }, params: { id: 'abc' }, requiredRoles: admin }, deleteProduct));
console.log(handle({ headers: { authorization: 'Bearer tok-admin' }, params: { id: '0' }, requiredRoles: admin }, deleteProduct));
console.log(handle({ headers: { authorization: 'Bearer tok-admin' }, params: { id: '7' }, requiredRoles: admin }, deleteProduct));

Output

401 Unauthorized
403 Forbidden resource
400 Validation failed (numeric string is expected)
400 id must be positive
200 deleted product 7

Implement a token bucket rate limiter (per key) that allows bursts.

Example

Each key has a bucket of capacity tokens that refills at ratePerSec. A request takes one token or is rejected with 429. Time is passed in, so it is easy to test.

class TokenBucket {
  private buckets = new Map<string, { tokens: number; last: number }>();
  constructor(private capacity: number, private ratePerSec: number) {}

  take(key: string, nowMs: number): boolean {
    const b = this.buckets.get(key) ?? { tokens: this.capacity, last: nowMs };
    const elapsed = (nowMs - b.last) / 1000;
    b.tokens = Math.min(this.capacity, b.tokens + elapsed * this.ratePerSec);
    b.last = nowMs;
    const allowed = b.tokens >= 1;
    if (allowed) b.tokens -= 1;
    this.buckets.set(key, b);
    return allowed;
  }
}

const limiter = new TokenBucket(5, 1);   // burst of 5, then 1 request per second
const log: string[] = [];
for (let i = 0; i < 7; i++) log.push(limiter.take('ip:10.0.0.1', 0) ? '200' : '429');
console.log('burst at t=0s :', log.join(' '));
console.log('t=1.0s        :', limiter.take('ip:10.0.0.1', 1000) ? '200' : '429');
console.log('t=1.5s        :', limiter.take('ip:10.0.0.1', 1500) ? '200' : '429');
console.log('t=3.0s        :', [limiter.take('ip:10.0.0.1', 3000), limiter.take('ip:10.0.0.1', 3000)].map((ok) => (ok ? '200' : '429')).join(' '));
console.log('other ip t=3s :', limiter.take('ip:10.0.0.2', 3000) ? '200' : '429');

Output

burst at t=0s : 200 200 200 200 200 429 429
t=1.0s        : 200
t=1.5s        : 429
t=3.0s        : 200 200
other ip t=3s : 200

Write retry with exponential backoff and jitter for calls to a flaky payment gateway.

Example

Retry only errors that are worth retrying (timeouts, 5xx), not 4xx. The delay doubles each time, is capped, and gets jitter so many clients do not retry at the same moment. A seeded random function keeps the output stable.

class GatewayError extends Error { constructor(readonly status: number, msg: string) { super(msg); } }

function seeded(seed: number) {
  return () => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; };
}
const random = seeded(42);
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function retry<T>(fn: () => Promise<T>, opts: { retries: number; baseMs: number; maxMs: number }): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (e) {
      const retryable = e instanceof GatewayError && (e.status >= 500 || e.status === 408);
      if (!retryable || attempt >= opts.retries) throw e;
      const exp = Math.min(opts.maxMs, opts.baseMs * 2 ** attempt);
      const delay = Math.round(exp / 2 + random() * (exp / 2));       // "equal jitter"
      console.log('  attempt', attempt + 1, 'failed (' + (e as GatewayError).status + '), retry in', delay, 'ms');
      await sleep(delay);
    }
  }
}

async function main() {
  let calls = 0;
  const flaky = async () => { calls++; if (calls < 3) throw new GatewayError(503, 'gateway busy'); return { paymentId: 'pay_N8kq2', status: 'captured' }; };
  console.log('result:', await retry(flaky, { retries: 4, baseMs: 10, maxMs: 80 }));

  const declined = async () => { throw new GatewayError(402, 'card declined'); };
  try { await retry(declined, { retries: 4, baseMs: 10, maxMs: 80 }); }
  catch (e) { console.log('not retried:', (e as Error).message); }
}
main();

Output

  attempt 1 failed (503), retry in 8 ms
  attempt 2 failed (503), retry in 15 ms
result: { paymentId: 'pay_N8kq2', status: 'captured' }
not retried: card declined

Implement a simple circuit breaker for calls to another microservice.

Example

After threshold consecutive failures the circuit opens and calls fail fast; after cooldownMs one trial call is allowed (half-open); success closes it again. A fake clock keeps it deterministic.

type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

class CircuitBreaker {
  private state: State = 'CLOSED';
  private failures = 0;
  private openedAt = 0;
  constructor(private threshold: number, private cooldownMs: number, private now: () => number) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (this.now() - this.openedAt < this.cooldownMs) throw new Error('503 circuit open, failing fast');
      this.state = 'HALF_OPEN';
    }
    try {
      const result = await fn();
      this.state = 'CLOSED';
      this.failures = 0;
      return result;
    } catch (e) {
      this.failures++;
      if (this.state === 'HALF_OPEN' || this.failures >= this.threshold) {
        this.state = 'OPEN';
        this.openedAt = this.now();
      }
      throw e;
    }
  }
  get status() { return this.state; }
}

let clock = 0;
let inventoryUp = false;
const breaker = new CircuitBreaker(3, 10_000, () => clock);
const reserve = async () => { if (!inventoryUp) throw new Error('ECONNREFUSED inventory:4001'); return 'reserved'; };

async function attempt(label: string) {
  try { console.log(label, '->', await breaker.call(reserve), '[' + breaker.status + ']'); }
  catch (e) { console.log(label, '->', (e as Error).message, '[' + breaker.status + ']'); }
}

async function main() {
  for (let i = 1; i <= 4; i++) await attempt('t=0s   call ' + i);
  clock = 5_000;  await attempt('t=5s   call 5');
  clock = 11_000; await attempt('t=11s  call 6 (trial)');
  inventoryUp = true;
  clock = 22_000; await attempt('t=22s  call 7 (trial)');
  await attempt('t=22s  call 8');
}
main();

Output

t=0s   call 1 -> ECONNREFUSED inventory:4001 [CLOSED]
t=0s   call 2 -> ECONNREFUSED inventory:4001 [CLOSED]
t=0s   call 3 -> ECONNREFUSED inventory:4001 [OPEN]
t=0s   call 4 -> 503 circuit open, failing fast [OPEN]
t=5s   call 5 -> 503 circuit open, failing fast [OPEN]
t=11s  call 6 (trial) -> ECONNREFUSED inventory:4001 [OPEN]
t=22s  call 7 (trial) -> reserved [CLOSED]
t=22s  call 8 -> reserved [CLOSED]