DRY Across Boundaries
When to Use
When dealing with knowledge duplication across system boundaries: microservices, frontend/backend, multiple programming languages, or separate repositories.
Decision Framework
| Boundary Type |
DRY Approach |
Implementation |
| Frontend <-> Backend |
Shared schema/types |
GraphQL schema, tRPC, Protocol Buffers, OpenAPI codegen |
| Multiple microservices |
Shared contracts |
API specs, event schemas, shared libraries (carefully) |
| Multiple languages |
Code generation |
Protocol Buffers, OpenAPI Generator, GraphQL Codegen |
| Multiple repos |
Shared packages |
npm packages, PyPI, Maven Central (versioned) |
| Database <-> Code |
Schema-driven |
ORM models, migrations, codegen from schema |
| Mobile <-> Backend |
API contracts |
OpenAPI/Swagger, GraphQL, REST with typed clients |
Pattern: Schema-Driven Development
// SINGLE SOURCE: Protocol Buffers schema
syntax = "proto3";
message User {
int32 id = 1;
string email = 2;
string name = 3;
UserType type = 4;
}
enum UserType {
REGULAR = 0;
VIP = 1;
GOLD = 2;
}
service UserService {
rpc CreateUser(CreateUserRequest) returns (User);
rpc GetUser(GetUserRequest) returns (User);
}
// GENERATED CODE (never hand-written):
// - user_pb2.py (Python)
// - user.pb.go (Go)
// - user_pb.ts (TypeScript)
// - User.java (Java)
// All languages now share exact same schema
// Type safety across boundaries
Pattern: API Contract Sharing (OpenAPI)
# openapi.yml — Single source of truth
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required: [id, email, name]
properties:
id: { type: integer }
email: { type: string, format: email }
name: { type: string }
type: { type: string, enum: [regular, vip, gold] }
# Generate client SDKs from OpenAPI spec
npx openapi-generator-cli generate \
-i openapi.yml \
-g typescript-fetch \
-o ./frontend/src/api
npx openapi-generator-cli generate \
-i openapi.yml \
-g python \
-o ./mobile-app/api
# Frontend and mobile now have type-safe clients
# Schema lives in ONE place
Pattern: Shared Validation Logic
// PROBLEM: Validation duplicated frontend/backend
// Frontend (JavaScript)
if (password.length < 8) {
throw new Error('Password too short');
}
// Backend (Python)
if len(password) < 8:
raise ValidationError('Password too short')
// SOLUTION: Shared schema
// schema.zod.ts (shared package)
import { z } from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1),
});
export type CreateUserData = z.infer<typeof CreateUserSchema>;
// Frontend uses directly
import { CreateUserSchema } from '@company/shared-schemas';
const data = CreateUserSchema.parse(formData);
// Backend: Convert Zod schema to JSON Schema, use in Python
// (via build step that generates JSON Schema -> Python Pydantic models)
Pattern: Monorepo with Shared Packages
# Repository structure
monorepo/
packages/
schemas/ # Shared schemas (Zod, JSON Schema, Protocol Buffers)
types/ # Shared TypeScript types
validation/ # Shared validation utilities
apps/
web-frontend/ # Imports from packages/
mobile-app/ # Imports from packages/
backend-api/ # Imports from packages/
package.json
# All apps import shared packages
# Single source of truth for schemas and types
# No duplication across boundaries
When Duplication Across Boundaries is OK
| Scenario |
Why Duplication is Acceptable |
| Microservices with different domains |
Services should be independently deployable; shared libraries create coupling |
| Different security contexts |
Backend validation is authoritative; frontend validation is UX convenience |
| Performance-critical paths |
Optimized implementations may differ even with same contract |
| Legacy integration |
Bridging old and new systems; duplication temporary during migration |
Common Mistakes
- Shared code libraries across microservices — Creates coupling, breaks independent deployability
- Frontend validation without backend validation — Security vulnerability; frontend is advisory only
- Manual synchronization of types — Guaranteed drift; always use code generation
- Tightly coupling services through shared database — Violates service boundaries; use event schemas instead
- No versioning on shared contracts — Breaking changes propagate uncontrollably
See Also