a single responsibility, explicit inputs/outputs, and no hidden side effects. Below is a complete implementation of a URL shortener system using FBD in TypeScript.
Step 1: Decomposition
The URL shortener is decomposed into four functional blocks:
- EndpointSanitizer: Validates and normalizes raw URL strings.
- CodeGenerator: Produces unique short codes with collision handling.
- LinkRegistry: Manages storage and retrieval of key-value pairs.
- RedirectEngine: Orchestrates the flow between blocks.
Step 2: Specification
Each block is defined by a TypeScript interface that serves as the contract for AI generation and integration.
// contracts.ts
export interface SanitizedEndpoint {
protocol: 'http' | 'https';
domain: string;
path: string;
}
export interface LinkRegistry {
store(code: string, url: string): void;
retrieve(code: string): string | undefined;
has(code: string): boolean;
}
export type CollisionChecker = (code: string) => boolean;
Step 3: Generation
Each block is generated independently based on its specification. The implementation details are isolated, ensuring no cross-block dependencies.
Block 1: EndpointSanitizer
// endpoint-sanitizer.ts
import { SanitizedEndpoint } from './contracts';
export function sanitizeEndpoint(rawInput: string): SanitizedEndpoint {
if (!rawInput || !rawInput.trim()) {
throw new Error('Input URL cannot be empty');
}
const trimmed = rawInput.trim();
let normalized = trimmed;
// Add protocol if missing
if (!/^[a-zA-Z]+:\/\//.test(normalized)) {
normalized = `https://${normalized}`;
}
const url = new URL(normalized);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`Unsupported protocol: ${url.protocol}`);
}
return {
protocol: url.protocol.replace(':', '') as 'http' | 'https',
domain: url.hostname,
path: url.pathname || '/',
};
}
Block 2: CodeGenerator
// code-generator.ts
import { CollisionChecker } from './contracts';
const BASE62_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const MAX_ATTEMPTS = 10;
export function generateShortCode(
targetUrl: string,
collisionCheck: CollisionChecker
): string {
if (!targetUrl) {
throw new Error('Target URL cannot be empty');
}
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const length = Math.floor(Math.random() * 3) + 6; // 6 to 8 chars
const code = Array.from({ length }, () =>
BASE62_CHARS[Math.floor(Math.random() * BASE62_CHARS.length)]
).join('');
if (!collisionCheck(code)) {
return code;
}
}
throw new Error('Failed to generate unique code after maximum attempts');
}
Block 3: LinkRegistry
// link-registry.ts
import { LinkRegistry } from './contracts';
export class InMemoryLinkRegistry implements LinkRegistry {
private storage = new Map<string, string>();
store(code: string, url: string): void {
if (!code || !url) {
throw new Error('Code and URL must not be empty');
}
this.storage.set(code, url);
}
retrieve(code: string): string | undefined {
return this.storage.get(code);
}
has(code: string): boolean {
return this.storage.has(code);
}
}
Step 4: Integration
The blocks are wired together in the integration layer. Dependencies are injected explicitly, ensuring loose coupling and testability.
// app.ts
import { sanitizeEndpoint } from './endpoint-sanitizer';
import { generateShortCode } from './code-generator';
import { InMemoryLinkRegistry } from './link-registry';
const registry = new InMemoryLinkRegistry();
export function createShortLink(rawUrl: string): string {
const endpoint = sanitizeEndpoint(rawUrl);
const code = generateShortCode(rawUrl, (c) => registry.has(c));
registry.store(code, rawUrl);
return code;
}
export function resolveShortLink(code: string): string | undefined {
return registry.retrieve(code);
}
Pitfall Guide
-
Leaky Abstractions
- Explanation: Blocks directly import or call other blocks, creating hidden dependencies.
- Fix: Use dependency injection and interfaces. Pass required functions as arguments rather than importing implementations.
-
Stateful AI Prompts
- Explanation: AI models lose context when generating multiple blocks in a single session, leading to inconsistent contracts.
- Fix: Generate each block in isolation with a self-contained specification. Never rely on cross-prompt memory.
-
Ignoring Error Boundaries
- Explanation: Blocks swallow errors or return inconsistent types, making debugging difficult.
- Fix: Define explicit error types and throw exceptions for invalid states. Ensure all blocks handle edge cases consistently.
-
Hardcoded Dependencies
- Explanation: Blocks assume specific implementations (e.g., hardcoded storage), reducing reusability.
- Fix: Abstract dependencies behind interfaces. Inject implementations at runtime to allow swapping (e.g., in-memory vs. database).
-
Over-Engineering Specs
- Explanation: Specifications become too verbose, confusing the AI and bloating the code.
- Fix: Focus on contracts, inputs/outputs, and error conditions. Omit implementation details unless critical.
-
Race Conditions in Generation
- Explanation: Concurrent requests may generate the same code before collision checks complete.
- Fix: Implement atomic operations or distributed locking in production storage blocks.
-
Missing Testability
- Explanation: Blocks are tightly coupled, making unit testing impossible.
- Fix: Design blocks to be pure functions or use dependency injection. Mock external dependencies in tests.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High Traffic | Redis/DB Storage Block | Persistence and speed under load | Higher infra cost |
| MVP/Prototype | In-Memory Storage Block | Fast development and zero setup | Data loss on restart |
| Strict Compliance | Audit Logging Block | Traceability and regulatory requirements | Moderate dev cost |
| Multi-Tenant | Namespaced Storage Block | Data isolation and security | Moderate infra cost |
Configuration Template
Use this template to define block specifications for AI generation:
// block-spec.template.ts
export interface BlockSpec {
name: string;
description: string;
inputs: { name: string; type: string; description: string }[];
outputs: { name: string; type: string; description: string }[];
errors: string[];
dependencies?: string[];
}
// Example usage
const endpointSanitizerSpec: BlockSpec = {
name: 'EndpointSanitizer',
description: 'Validates and normalizes raw URL strings.',
inputs: [
{ name: 'rawInput', type: 'string', description: 'The raw URL string.' },
],
outputs: [
{ name: 'sanitizedEndpoint', type: 'SanitizedEndpoint', description: 'Normalized URL object.' },
],
errors: ['EmptyInput', 'UnsupportedProtocol', 'InvalidFormat'],
};
Quick Start Guide
- Install Dependencies: Run
npm init -y && npm install typescript @types/node.
- Create Contracts: Define interfaces for inputs, outputs, and dependencies.
- Generate Blocks: Use AI to generate each block based on its specification.
- Wire Integration: Implement the integration layer with dependency injection.
- Run Tests: Execute unit tests to validate block behavior and contracts.
This methodology ensures that AI-generated code is modular, testable, and production-ready, enabling teams to scale efficiently without sacrificing quality.