Summary
Structure prompts using onion architecture with four layers: Core (universal rules), Domain (project patterns), Application (feature context), and Task (specific request). This approach enables independent versioning, layer reuse, A/B testing, and integration with prompt caching for 90% cost reduction. Each layer changes at different frequencies, making prompts maintainable and composable.
The Problem
Monolithic prompts that dump all context at once are difficult to maintain, version control, and reuse. When working with multiple projects or iterating on development approaches, you must either duplicate context across projects or manually extract reusable pieces. Mixing stable rules (coding standards) with dynamic requests (current task) prevents prompt caching optimizations, adding unnecessary cost. Changes to shared patterns require editing every prompt using that pattern.
The Solution
Layer prompts using onion architecture: Core (universal principles that apply everywhere), Domain (project-specific patterns), Application (feature context), and Task (specific request). Each layer changes at different frequencies – Core rarely changes, Domain changes per project, Application changes per feature, Task changes per request. This separation enables independent versioning, layer reuse across projects, A/B testing by swapping layers, and full compatibility with prompt caching. The immutable outer layers cache efficiently while inner layers remain dynamic.
The Problem
Most developers structure prompts as a single monolithic block:
You are an expert TypeScript developer.
Follow functional programming principles.
Write tests for all functions.
This project uses factory pattern, no classes.
Database access via Supabase client.
All errors use custom error classes.
We're building a social media scheduler.
Posts are stored in 'posts' table.
Support Instagram, Twitter, LinkedIn.
Add Instagram post validation.
Check image dimensions and aspect ratio.
Return detailed error messages.
Why This Approach Fails
1. Maintenance Nightmare
When you decide “all errors should use custom error classes,” you must update this rule in:
- Every project prompt
- Every cached prompt
- Every documentation file
Changes to shared patterns require finding and editing every location that mentions them.
2. No Reusability
The “TypeScript expert” layer is identical across projects, but duplicating it means:
- Manual copy-paste across prompts
- Risk of drift (one project gets updated, another doesn’t)
- Version confusion (which version of the standard is this prompt using?)
3. Breaks Prompt Caching
Prompt caching only works when stable content is at the beginning and dynamic content is at the end. Mixing them prevents caching:
❌ Problem:
[Universal rules (stable)]
[Project patterns (stable)]
[Task-specific request (dynamic)] ← Cache breaks here
[Specific implementation details (dynamic)]
Without caching, every request pays full price for repeated context.
4. Difficult A/B Testing
Can’t easily test “What if we used composition over inheritance?” because the principle is buried in a monolithic block. To test, you must:
- Copy the entire prompt
- Find the right sentence
- Edit it
- Track which version is which
5. Version Control Chaos
When you update coding standards, the diff shows the entire prompt changing, making it impossible to see what actually changed:
- You are an expert TypeScript developer.
+ You are a world-class TypeScript developer.
(rest of prompt identical but shows as changed)
The Solution: Layered Prompts
Instead of a monolithic block, structure prompts as four layers with increasing specificity:
Layer 1: Core (Universal, Rarely Changes)
Principles that apply to all projects:
## Core Development Principles
You are an expert TypeScript developer.
Follow functional programming principles.
Write tests for all functions.
Avoid side effects where possible.
Use type safety as a quality gate.
Favor composition over inheritance.
Characteristics:
- Universal across all projects
- Rarely changes (maybe quarterly)
- Most stable layer
- Foundation for everything else
Layer 2: Domain (Project-Specific, Changes Per Project)
Patterns specific to this project:
## Project Architecture (Acme Social Scheduler)
This project uses the factory pattern for service creation (no classes).
Database access is exclusively through the Supabase client.
All errors must use custom error classes from @acme/errors.
API responses use tRPC with Zod validation.
Test files live alongside source files: `feature.ts` + `feature.test.ts`.
Characteristics:
- Project-specific patterns
- Changes when project architecture evolves
- Shared by all features in the project
- Independent of current task
Layer 3: Application (Feature Context, Changes Per Feature)
Context for this feature:
## Social Media Scheduler Feature
We're building a scheduler that manages posts across multiple platforms.
Posts are stored in the 'posts' table with columns: id, title, content, scheduled_at, platforms, status.
Supported platforms: Instagram, Twitter, LinkedIn.
Scheduled posts must be queued for delivery at their scheduled_at timestamp.
Characteristics:
- Feature-specific context
- Changes when working on different features
- Includes data models, workflows, constraints
- Stable during implementation of a single feature
Layer 4: Task (Specific Request, Changes Per Request)
The specific task for this request:
## Current Task: Instagram Post Validation
Add validation for Instagram posts before scheduling.
Validation rules:
1. Image must be square (1:1 aspect ratio) or vertical (4:5)
2. Image dimensions must be >= 1080x1080px
3. Caption must be <= 2200 characters
4. Hashtags must be <= 30 per post
Implement as a validation function: `validateInstagramPost(post: Post): ValidationError[]`
Return detailed error messages for each validation failure.
Characteristics:
- Specific to this request
- Changes on every request
- Most dynamic layer
- Where you put task-specific details
Implementation Pattern
Basic Structure
const layeredPrompt = [
coreLayer, // Universal principles
domainLayer, // Project patterns
applicationLayer, // Feature context
taskLayer, // Specific request
].join('\n\n---\n\n');
// Result:
// [Core principles]
// ---
// [Project patterns]
// ---
// [Feature context]
// ---
// [Specific task]
Complete Example
const coreLayer = `
## Core Development Principles
You are an expert TypeScript developer.
Follow functional programming principles.
Write tests for all functions.
Use type safety as a quality gate.
Favor composition over inheritance.
Avoid side effects in pure functions.
`;
const domainLayer = `
## Project Architecture (Acme Social Scheduler)
This project uses factory pattern for services (no classes).
Database access via Supabase client only.
All errors use custom error classes from @acme/errors.
API endpoints use tRPC with Zod validation.
Test files colocated with source: feature.ts + feature.test.ts.
`;
const applicationLayer = `
## Social Media Scheduler Feature
Schedules posts across Instagram, Twitter, LinkedIn.
Posts stored in 'posts' table: id, title, content, scheduled_at, platforms, status.
Delivery happens via background workers polling the queue.
`;
const taskLayer = `
## Current Task: Instagram Post Validation
Implement validation function: validateInstagramPost(post: Post): ValidationError[]
Rules:
- Image: square (1:1) or vertical (4:5), >= 1080x1080px
- Caption: <= 2200 characters
- Hashtags: <= 30
Return detailed errors for each violation.
`;
const prompt = [
coreLayer,
domainLayer,
applicationLayer,
taskLayer,
].join('\n\n---\n\n');
const response = await client.messages.create({
model: 'claude-sonnet-4',
max_tokens: 4096,
messages: [
{
role: 'user',
content: prompt,
},
],
});
Key Benefits
1. Independent Versioning
Each layer can be versioned independently:
const v2_coreLayer = `
## Core Development Principles (v2)
- New: "Favor explicit over implicit"
- New: "Use strict null checking everywhere"
`;
const v1_domainLayer = `
## Project Architecture (v1)
(unchanged from before)
`;
// Mix versions freely
const prompt = [
v2_coreLayer, // Using new version
v1_domainLayer, // Still on v1
applicationLayer,
taskLayer,
].join('\n\n---\n\n');
2. Layer Reusability
Reuse the same coreLayer across all projects:
// Project A
const promptA = [
coreLayer, // Same everywhere
projectA_domainLayer,
projectA_appLayer,
taskA,
].join('\n\n---\n\n');
// Project B
const promptB = [
coreLayer, // Same universal principles
projectB_domainLayer,
projectB_appLayer,
taskB,
].join('\n\n---\n\n');
3. A/B Testing
Swap layers to test different approaches:
// Test 1: Current approach
const prompt_v1 = [
coreLayer,
domainLayerV1, // "Favor composition over inheritance"
applicationLayer,
taskLayer,
].join('\n\n---\n\n');
// Test 2: Alternative approach
const prompt_v2 = [
coreLayer,
domainLayerV2, // "Favor inheritance for shared behavior"
applicationLayer,
taskLayer,
].join('\n\n---\n\n');
const [response1, response2] = await Promise.all([
client.messages.create({ messages: [{ role: 'user', content: prompt_v1 }] }),
client.messages.create({ messages: [{ role: 'user', content: prompt_v2 }] }),
]);
// Compare outputs
4. Prompt Caching Optimization
When combined with prompt caching, outer layers cache efficiently:
const response = await client.messages.create({
model: 'claude-sonnet-4',
max_tokens: 4096,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: [coreLayer, domainLayer, applicationLayer].join('\n\n---\n\n'),
cache_control: { type: 'ephemeral' }, // Mark for caching
},
{
type: 'text',
text: taskLayer, // Dynamic, not cached
},
],
},
],
});
// Cost breakdown:
// First request:
// Cached layers: 4K tokens × $0.003 = $0.012
// Dynamic layer: 100 tokens × $0.003 = $0.0003
// Total: $0.0123
//
// Subsequent requests (within 5 min):
// Cached layers: 4K tokens × $0.0003 = $0.0012
// Dynamic layer: 100 tokens × $0.003 = $0.0003
// Total: $0.0015 (88% savings)
5. Clear Separation of Concerns
Each layer has a single responsibility:
- Core: Universal development principles
- Domain: Project-specific architecture
- Application: Feature context and data models
- Task: Specific implementation request
This clarity makes prompts easier to understand and maintain.
Implementation Patterns
Pattern 1: Storage in Files
// core-layer.md
export const coreLayer = fs.readFileSync('layers/core.md', 'utf-8');
// domain-layer.ts
export function getDomainLayer(projectName: string) {
return fs.readFileSync(`layers/${projectName}/domain.md`, 'utf-8');
}
// application-layer.ts
export function getApplicationLayer(feature: string) {
return fs.readFileSync(`layers/applications/${feature}.md`, 'utf-8');
}
// Usage
const prompt = [
coreLayer,
getDomainLayer('acme'),
getApplicationLayer('social-scheduler'),
`Implement user authentication`,
].join('\n\n---\n\n');
Pattern 2: Programmatic Layer Building
interface LayerConfig {
coreVersion: 'v1' | 'v2';
projectId: string;
feature: string;
task: string;
}
function buildLayeredPrompt(config: LayerConfig): string {
const coreLayer = config.coreVersion === 'v1'
? getCoreLayerV1()
: getCoreLayerV2();
const domainLayer = getDomainLayer(config.projectId);
const appLayer = getApplicationLayer(config.projectId, config.feature);
return [
coreLayer,
domainLayer,
appLayer,
config.task,
].join('\n\n---\n\n');
}
// Usage
const prompt = buildLayeredPrompt({
coreVersion: 'v2',
projectId: 'acme',
feature: 'social-scheduler',
task: 'Implement Instagram validation',
});
Pattern 3: Dynamic Layer Selection
function selectLayers(context: Context) {
// Choose layers based on runtime context
const layers = [
coreLayer, // Always included
];
// Add domain layer only if project supports it
if (context.project?.hasArchitecture) {
layers.push(getDomainLayer(context.project.id));
}
// Add application layer only for feature work
if (context.feature) {
layers.push(getApplicationLayer(context.feature));
}
// Always add task layer
layers.push(context.task);
return layers.join('\n\n---\n\n');
}
Best Practices
1. Keep Layers Stable
Problem: Frequently changing layers break caching and reduce reusability.
Solution: Only change layers when the information genuinely applies to multiple requests.
// ❌ Bad: Task-specific details in application layer
const appLayer = `
We're building a social media scheduler.
Right now we need to validate Instagram posts.
Images must be square or vertical.
`;
// ✅ Good: Only general feature context in application layer
const appLayer = `
We're building a social media scheduler.
Posts are stored in 'posts' table.
Supported platforms: Instagram, Twitter, LinkedIn.
`;
// Task-specific details go in task layer
const taskLayer = `
Implement Instagram post validation.
Images must be square (1:1) or vertical (4:5).
`;
2. Use Clear Delimiters
Make it obvious where each layer begins and ends:
## LAYER 1: CORE PRINCIPLES
[Content]
---
## LAYER 2: PROJECT ARCHITECTURE
[Content]
---
## LAYER 3: FEATURE CONTEXT
[Content]
---
## LAYER 4: SPECIFIC TASK
[Content]
3. Document Layer Boundaries
Make it clear which information belongs in which layer:
interface LayerGuideline {
name: string;
changeFrequency: string;
examples: string[];
}
const guidelines: Record<string, LayerGuideline> = {
core: {
name: 'Core',
changeFrequency: 'Quarterly or when principles change',
examples: [
'Language expertise (TypeScript)',
'Development paradigms (functional)',
'Quality standards (write tests)',
],
},
domain: {
name: 'Domain',
changeFrequency: 'When project architecture evolves',
examples: [
'Design patterns (factory pattern)',
'Technology choices (Supabase)',
'Error handling approach (custom classes)',
],
},
application: {
name: 'Application',
changeFrequency: 'When switching features',
examples: [
'Feature name (social scheduler)',
'Data models (posts table)',
'Supported integrations (Instagram, Twitter)',
],
},
task: {
name: 'Task',
changeFrequency: 'Every request',
examples: [
'Specific implementation (add validation)',
'Current requirements (square aspect ratio)',
'Return format (ValidationError[])',
],
},
};
4. Test Layer Combinations
Ensure layers work well together:
test('core + domain layers produce valid prompts', () => {
const prompt = [coreLayer, domainLayer].join('\n\n---\n\n');
expect(prompt).toContain('TypeScript');
expect(prompt).toContain('factory pattern');
expect(prompt.length).toBeGreaterThan(500);
});
test('all four layers produce coherent prompt', () => {
const prompt = [
coreLayer,
domainLayer,
applicationLayer,
taskLayer,
].join('\n\n---\n\n');
// Verify no contradictions
const mentionsFactory = prompt.includes('factory');
const mentionsClasses = prompt.includes('classes');
expect(!(mentionsFactory && mentionsClasses)).toBe(true);
});
5. Version Layers Explicitly
Track which version of each layer you’re using:
interface PromptVersion {
coreVersion: string;
domainVersion: string;
applicationVersion: string;
taskDescription: string;
}
const versionHistory: PromptVersion[] = [
{
coreVersion: 'v1',
domainVersion: 'acme-v2',
applicationVersion: 'scheduler-v1',
taskDescription: 'Instagram validation',
},
];
// In git commits
git commit -m "Update core layer to v2: add explicit-over-implicit principle"
git commit -m "Update acme domain to v3: switch to composition pattern"
Combining with Prompt Caching
Layered prompts work beautifully with prompt caching:
const cachedLayers = [
coreLayer, // Cached: rarely changes
domainLayer, // Cached: stable per project
applicationLayer, // Cached: stable per feature
].join('\n\n---\n\n');
const response = await client.messages.create({
model: 'claude-sonnet-4',
max_tokens: 4096,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: cachedLayers,
cache_control: { type: 'ephemeral' }, // Enable caching
},
{
type: 'text',
text: taskLayer, // Dynamic layer not cached
},
],
},
],
});
// Results:
// - First request of day: ~$0.012 (full cost)
// - Next 49 requests: ~$0.0015 each (88% savings)
// - Daily savings: ~$0.45
// - Monthly savings: ~$10
// - Yearly savings: ~$120
Measuring Success
Metrics to Track
-
Layer Reusability: How many prompts use each layer?
coreLayer: used in 100% of prompts domainLayer: used in 95% of project A prompts applicationLayer: used in 87% of feature A prompts -
Cache Hit Rate: How often are cached layers used?
Target: >80% of requests hit the cached layers -
Maintenance Burden: How often do layers change?
coreLayer: 2-4 times/year domainLayer: 1-2 times/quarter applicationLayer: 1-2 times/week -
Cost Savings: Combined with caching
Without layering: $45/month With layering only: $35/month (22% savings from reduced duplication) With layering + caching: $5/month (89% savings)
Common Pitfalls
❌ Pitfall 1: Too Many Layers
Problem: Creating layers for every detail creates confusion
// ❌ Too many layers
const layers = [
coreLayer,
domainLayer,
architectureLayer,
databaseLayer,
apiLayer,
authenticationLayer,
applicationLayer,
taskLayer,
]; // 8 layers - too many to manage
Solution: Stick to 4 layers max
// ✅ Right number of layers
const layers = [
coreLayer,
domainLayer,
applicationLayer,
taskLayer,
]; // 4 layers - clear and manageable
❌ Pitfall 2: Mixing Change Frequencies
Problem: Putting frequently-changing content in cached layers
// ❌ Wrong: task details in application layer
const appLayer = `
We're building a scheduler.
This week we're implementing Instagram validation.
Next week we'll add Twitter support.
`;
Solution: Put only stable content in outer layers
// ✅ Correct: only architectural details
const appLayer = `
We're building a scheduler.
Supported platforms: Instagram, Twitter, LinkedIn.
Posts stored in 'posts' table.
`;
❌ Pitfall 3: Inconsistent Formatting
Problem: Different formatting breaks byte-for-byte matching for caching
// ❌ Request 1
const domainLayer1 = 'Errors use CustomError class';
// ❌ Request 2 (with extra spaces)
const domainLayer2 = 'Errors use CustomError class';
// Different bytes = cache miss
Solution: Normalize and freeze layer content
// ✅ Correct: keep content immutable
export const domainLayer = `
Errors use CustomError class.
Database via Supabase client.
`;
// Every reference uses identical content
❌ Pitfall 4: Duplicating Core Content
Problem: Different versions of the core layer across projects
// ❌ Project A
const coreA = `You are an expert TypeScript developer.`;
// ❌ Project B (subtly different)
const coreB = `You are a world-class TypeScript developer.`;
// Different content = can't reuse
Solution: Maintain single version of core layer
// ✅ Correct: share across all projects
export const coreLayer = `You are an expert TypeScript developer.`;
// Every project uses the same version
Related Concepts
- Chain-of-Thought Prompting – Apply reasoning at each layer
- Declarative Constraints Prompting – Define layer requirements declaratively
- Few-Shot Prompting with Project Examples – Store examples in Domain or Application layers
- Multi-Step Prompt Workflows – Use layers across workflow steps
- Progressive Disclosure Context – Load layers progressively as needed
- Explicit Constraints and Non-Goals – Define scope at each layer
- Prompt Caching Strategy – Cache outer layers for 90% cost reduction
- Hierarchical Context Patterns – Apply layering to project documentation
- Semantic Naming Patterns – Name layers consistently for discovery
Conclusion
Layered prompts transform prompt management from monolithic chaos to composable architecture:
Key Benefits:
- Independent versioning – Update layers without affecting others
- Reusability – Share core/domain layers across projects
- A/B testing – Swap layers to test different approaches
- Caching integration – Outer layers cache for 90% cost reduction
- Maintainability – Clear separation of concerns
Implementation Steps:
- Extract universal principles → Core layer
- Extract project patterns → Domain layer
- Extract feature context → Application layer
- Keep task-specific details → Task layer
- Combine with cache control for maximum efficiency
The result is a prompt architecture that scales with your complexity while remaining maintainable and cost-effective.
References
- Prompt Caching Strategy – How to structure prompts for maximum caching efficiency and 90% cost reduction
- Hierarchical CLAUDE.md Files – Apply the same layering approach to project documentation
- Onion Architecture Pattern – Original onion architecture concept that inspired prompt layering

