d by colons (:), and key-value assignments use equals signs (=). This syntax allows for flexible, extensible identifiers without imposing a rigid taxonomy.
Example DRI:
billing:transactions/order:id=TXN-9928471
- Context:
billing:transactions
- Resource:
order:id=TXN-9928471
The consumer constructs the DRI using data already available. No external API call is required to generate the identifier. A service receiving an order ID knows the context and builds the DRI immediately.
TypeScript Implementation
The following implementation demonstrates the gateway architecture, resolver interface, and enrichment capabilities.
// Core types
interface ResourceIdentifier {
context: string;
resource: string;
metadata?: Record<string, string>;
}
interface ResolutionContext {
[key: string]: string;
}
interface ResolverResponse<T> {
data: T;
source: string;
resolvedAt: Date;
}
// Resolver contract
interface ResourceResolver<T> {
resolve(identifier: ResourceIdentifier, context?: ResolutionContext): Promise<ResolverResponse<T>>;
supportsContext(context: string): boolean;
}
// Gateway implementation
class DelegatedGateway {
private resolvers: Map<string, ResourceResolver<unknown>> = new Map();
private defaultResolver?: ResourceResolver<unknown>;
register<T>(context: string, resolver: ResourceResolver<T>): void {
this.resolvers.set(context, resolver as ResourceResolver<unknown>);
}
setDefaultResolver<T>(resolver: ResourceResolver<T>): void {
this.defaultResolver = resolver as ResourceResolver<unknown>;
}
async resolve<T>(dri: string, context?: ResolutionContext): Promise<ResolverResponse<T>> {
const parsed = this.parseDRI(dri);
const resolver = this.resolvers.get(parsed.context) || this.defaultResolver;
if (!resolver) {
throw new Error(`No resolver registered for context: ${parsed.context}`);
}
return resolver.resolve(parsed, context) as Promise<ResolverResponse<T>>;
}
private parseDRI(dri: string): ResourceIdentifier {
const [context, resource] = dri.split('/');
return { context, resource };
}
}
// Concrete resolver example
class TransactionResolver implements ResourceResolver<Transaction> {
supportsContext(context: string): boolean {
return context === 'billing:transactions';
}
async resolve(
identifier: ResourceIdentifier,
context?: ResolutionContext
): Promise<ResolverResponse<Transaction>> {
// Extract transaction ID from resource string
const idMatch = identifier.resource.match(/id=([^&]+)/);
if (!idMatch) {
throw new Error('Invalid resource format: missing id');
}
const transactionId = idMatch[1];
// Resolution logic: determine actual endpoint based on context
// Example: Use date from context to select API version
const apiVersion = context?.date
? this.selectVersionByDate(context.date)
: 'v2';
const response = await fetch(
`https://api.billing.internal/${apiVersion}/transactions/${transactionId}`
);
return {
data: await response.json(),
source: `billing-internal-${apiVersion}`,
resolvedAt: new Date()
};
}
private selectVersionByDate(dateStr: string): string {
const date = new Date(dateStr);
return date < new Date('2024-01-01') ? 'v1' : 'v2';
}
}
// Usage
const gateway = new DelegatedGateway();
gateway.register('billing:transactions', new TransactionResolver());
// Basic resolution
const result = await gateway.resolve<Transaction>(
'billing:transactions/order:id=TXN-9928471'
);
// Resolution with enrichment
const enrichedResult = await gateway.resolve<Transaction>(
'billing:transactions/order:id=TXN-9928471',
{ date: '2023-06-15' }
);
Architecture Decisions
- Gateway as Resolution Hub: The gateway centralizes routing logic. This eliminates duplication across consumers and provides a single point for monitoring, caching, and error handling.
- Resolver Interface: Resolvers implement a strict contract. This allows independent development of resolvers and enables hot-swapping implementations without affecting the gateway.
- Context Enrichment: The pattern supports adding metadata at query time. The persisted DRI remains stable, but consumers can pass additional context (like dates or preferences) to influence resolution. This enables version selection and fallback strategies without modifying stored data.
- Default Resolver: The gateway supports a default resolver for contexts that don't require explicit routing. This simplifies identifiers when a single domain handles most requests.
- Opaque Resource Strings: The gateway treats the resource portion as an opaque string. This allows each domain to define its own resource syntax without coordinating with the gateway team.
Pitfall Guide
1. The "URL in Disguise" Trap
Explanation: Developers embed path segments or version numbers in the resource portion, recreating URL brittleness.
Fix: Keep the resource portion abstract. Use identifiers and key-value pairs that remain stable regardless of API structure. Let the resolver handle path construction.
2. Gateway as Single Point of Failure
Explanation: Centralizing resolution creates a dependency bottleneck. If the gateway fails, all cross-service references break.
Fix: Implement circuit breakers, caching layers, and health checks in the gateway. Consider deploying the gateway as a sidecar or embedded library for critical paths.
3. Resolver Coupling to Consumer Logic
Explanation: Resolvers become aware of consumer-specific requirements, violating separation of concerns.
Fix: Resolvers should only know how to locate resources. Consumer preferences should be passed as metadata, not hardcoded into resolver logic.
4. Ignoring Resolver Versioning
Explanation: Resolver implementations change over time, potentially breaking existing resolution behavior.
Fix: Version resolvers independently. Use semantic versioning for resolver contracts and maintain backward compatibility during transitions.
5. Client-Side DRI Parsing
Explanation: Consumers attempt to parse or manipulate DRI strings, creating hidden dependencies.
Fix: Treat DRIs as opaque blobs to consumers. Provide SDKs that generate DRIs from domain objects without exposing the syntax.
6. Missing Fallback Strategies
Explanation: Resolvers fail silently when resources move or become unavailable.
Fix: Implement fallback chains in resolvers. Try primary sources first, then secondary sources, and finally return structured errors with retry hints.
7. Over-Normalizing Context
Explanation: Creating deeply nested contexts with excessive colons makes routing complex and error-prone.
Fix: Limit context depth to two or three levels. Use metadata for additional granularity rather than expanding the context hierarchy.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Stable monolith with fixed endpoints | Direct URL Storage | Simplicity outweighs flexibility benefits | Low |
| Microservices with frequent migrations | DRI Pattern | Decouples identity from volatile locations | Medium (Gateway overhead) |
| Multi-tenant SaaS with regional endpoints | DRI with Context Enrichment | Enables dynamic routing based on tenant metadata | Medium-High |
| Legacy system integration | DRI with Adapter Resolver | Wraps legacy APIs behind stable identifiers | Low-Medium |
| High-throughput read-heavy workload | DRI with Gateway Caching | Reduces resolver load while maintaining flexibility | Medium |
Configuration Template
// gateway.config.ts
import { DelegatedGateway } from './gateway';
import { TransactionResolver } from './resolvers/transaction';
import { UserProfileResolver } from './resolvers/user-profile';
import { InventoryResolver } from './resolvers/inventory';
export function configureGateway(): DelegatedGateway {
const gateway = new DelegatedGateway();
// Register domain resolvers
gateway.register('billing:transactions', new TransactionResolver());
gateway.register('identity:profiles', new UserProfileResolver());
gateway.register('warehouse:inventory', new InventoryResolver());
// Configure default resolver for unregistered contexts
gateway.setDefaultResolver(new InventoryResolver());
// Optional: Configure resolver preferences
gateway.setResolverOptions({
timeout: 5000,
retries: 2,
cacheTTL: 300 // seconds
});
return gateway;
}
Quick Start Guide
- Define your DRI: Construct an identifier using
<context>/<resource> syntax. Example: billing:transactions/order:id=TXN-123.
- Create a Resolver: Implement the
ResourceResolver interface for your domain. Handle resource extraction and endpoint resolution.
- Register with Gateway: Add your resolver to the gateway using the context key.
- Resolve References: Call
gateway.resolve(dri) to fetch resources. Pass enrichment context as needed.
- Store DRIs: Replace URL storage with DRI strings in your database. References remain stable across infrastructure changes.