Quick Navigation
- Start here — Solution design
- Application design
- Integration patterns
- API design
- Interface design
- Data design
- Process design
- Putting it together
- Cheat sheet
Before we start — the one thing to hold onto
A decomposed system isn't useful until you define how its parts are structured, how they talk, what contracts they follow, and how data and workflows flow between them. I've seen clean component models that couldn't implement the business process — and messy systems where every boundary was an illusion because two components could write the same data.
Solution design is the set of layered decisions — application structure, integration patterns, API contracts, interface design, data ownership, and process mapping — that make a decomposition real.
Each layer answers a different question: Application — what is each part responsible for? Integration — how do parts communicate? API — what promises do parts make to each other? Interface — what do users and systems see? Data — who owns what, and how does it flow? Process — how do business workflows become system behaviour?
These layers aren't independent — they constrain and inform each other. The integration pattern you choose affects API design. The data ownership model affects integration. Process design ties everything together.
Keep it in mind.
1. Solution design — Six layers of decisions from structure to workflow
Once you've broken a system into parts, you must decide how each part is structured internally, how parts communicate, what contracts they follow, how data moves, and how business processes map to system behaviour. Solution design is the bridge between "what are the components?" and "how do they actually work together?"
Solution design = six layers of decisions from structure to workflow.
2. Application design — Boundaries, responsibilities, and internal structure
You've decomposed a system into components. Now you must decide what each component is responsible for internally — its boundaries, its responsibilities, and its internal structure.
Application design answers: "Inside this boundary, what does this component do, and how is it organised?"
Key decisions: Responsibility scope — what this component owns and what it doesn't. Internal layers — how the component is structured (presentation, business logic, data access). Entry points — how other components and users interact with it. Internal contracts — how the parts within the component relate to each other.
A well-designed component has: A single, clear responsibility (high cohesion). No knowledge of other components' internals (low coupling). Clean internal structure that supports change. Explicit entry points — no reaching in from the outside.
Common internal structures: Layered — Presentation → Business → Data. Best for CRUD-heavy applications with clear separation of concerns. Hexagonal — Core domain ↔ Ports ↔ Adapters. Best for domain-rich applications that need testability and technology independence. Onion — Domain → Application → Infrastructure. Best for applications where the domain model is the most important concern. Vertical slices — Feature-based modules. Best for applications where features are independent and change frequently.
Responsibility definition template: Component: [Name]. Owns: [What data and capabilities this component controls]. Provides: [What other components can request from it]. Consumes: [What this component needs from others]. Does NOT do: [Explicit exclusions to prevent scope creep].
Application design = what each component does, and how it's organised inside.
Try it yourself — The component check
Take one of your components. Can you answer:
| Question | Your answer |
|---|---|
| What does it own? | |
| What does it provide? | |
| What does it consume? | |
| What does it explicitly NOT do? |
If you can't answer the last one, your component has scope creep.
3. Integration patterns — Synchronous, asynchronous, event-driven, and batch
Components need to communicate. How they communicate determines the coupling, the reliability, and the complexity of the entire system.
There are four fundamental integration patterns, each with distinct trade-offs:
flowchart TD
INT["🔗 Integration Patterns"]
INT --> SYNC["📞 Synchronous\nRequest → Wait → Response\nTight coupling, real-time"]
INT --> ASYNC["📬 Asynchronous\nSend → Continue → Process later\nLoose coupling, eventual consistency"]
INT --> EVENT["📢 Event-Driven\nPublish → Subscribe\nDecoupled, reactive"]
INT --> BATCH["📦 Batch\nCollect → Process → Deliver\nScheduled, bulk processing"]
Synchronous — the caller waits for the response. Simple to understand, but creates tight coupling and fragility when the called component is slow or down. Asynchronous — the caller sends a message and continues. The receiver processes it later. Loose coupling, but requires handling eventual consistency. Event-driven — components publish events. Other components subscribe and react. Maximum decoupling, but harder to trace and debug. Batch — data is collected and processed in bulk on a schedule. Efficient for large volumes, but introduces latency.
Pattern comparison:
| Pattern | Coupling | Consistency | Latency | Complexity | Error handling |
|---|---|---|---|---|---|
| Synchronous (REST/gRPC) | Tight | Immediate | Low | Low | Caller handles errors directly |
| Asynchronous (Messaging) | Medium | Eventual | Medium | Medium | Dead letter queues, retries |
| Event-driven (Pub/Sub) | Loose | Eventual | Variable | High | Event sourcing, idempotency |
| Batch (ETL/ELT) | Loose | Delayed | High | Medium | Retry whole batch, data validation |
When to use each: User needs an immediate answer → Synchronous. User is waiting — latency matters. Order placed, payment needs processing → Asynchronous messaging. Payment may take time; don't block the user. Inventory changed, multiple systems need to know → Event-driven. Many consumers, decoupled reaction. Nightly data warehouse refresh → Batch. Volume is large, latency is acceptable.
The integration pattern you choose determines the coupling, the reliability characteristics, and the complexity of error handling across the system.
Try it yourself — The pattern test
Take three integrations in your system. What pattern are they using? Should they be?
| Integration | Current pattern | Should it be? | Why? |
|---|---|---|---|
If you defaulted to REST for everything, you haven't chosen — you've drifted.
4. API design — Contracts, versioning, REST vs GraphQL vs messaging
Components communicate through interfaces. The API is the contract — the promise one component makes to another about what it provides, how to ask for it, and what to expect in return.
A bad API creates coupling, confusion, and breakage. A good API creates independence, clarity, and stability.
Key API design decisions: What to expose — only what consumers need, not the internal model. How to structure — resources, operations, events, or queries. How to version — so contracts can evolve without breaking consumers. How to handle errors — so callers know what went wrong and what to do about it.
flowchart TD
API["🤝 API Design"]
API --> REST["REST\nResources + HTTP verbs\nStateless, cacheable"]
API --> GQL["GraphQL\nQuery language\nFlexible, client-driven"]
API --> GRPC["gRPC\nBinary protocol\nFast, typed contracts"]
API --> MSG["Messaging\nQueues + topics\nAsync, decoupled"]
API --> VER["Versioning\nURL · Header · Negotiation"]
API --> ERR["Error handling\nStandard codes · Problem details"]
API style comparison:
| Style | Best for | Strengths | Weaknesses |
|---|---|---|---|
| REST | Web APIs, CRUD operations | Simple, well-understood, cacheable | Over-fetching, under-fetching, chatty |
| GraphQL | Complex queries, mobile clients | Flexible queries, single endpoint | Caching complexity, N+1 risk, learning curve |
| gRPC | Internal service-to-service | Fast, typed, streaming support | Not human-readable, binary protocol |
| Messaging (queues/topics) | Async integration | Decoupled, reliable, scalable | Eventual consistency, harder to trace |
API versioning strategies: URL path — /v1/orders. Simple, visible — but URL isn't the right place for version. Header — Accept: application/vnd.api+json;version=2. Clean URLs — but less visible. Query parameter — /orders?version=2. Easy to implement — but pollutes the resource URL. Content negotiation — Accept: application/json; profile="/schemas/v2". Most correct — but complex.
API design principles: Design for the consumer, not the implementation. Be explicit about what is stable and what may change. Use consistent error formats (RFC 7807 Problem Details). Document contracts with OpenAPI/AsyncAPI specifications. Treat API changes as breaking changes by default.
An API isn't a technical detail — it's an architectural commitment. Changing it later means coordinating with every consumer.
Try it yourself — The API check
Take your most important API. Run it through the check:
| Question | Your answer |
|---|---|
| Is it designed for the consumer or the implementation? | |
| Are changes treated as breaking by default? | |
| Is it documented with OpenAPI/AsyncAPI? | |
| Is the versioning strategy consistent? |
If any answer is "no," your API is a liability, not an asset.
5. Interface design — User interfaces, system interfaces, and consistency
Interfaces are what people and systems actually see and use. A system with great internal architecture but poor interfaces fails in practice.
Interface design covers two domains: User interfaces — what humans see, touch, and interact with. System interfaces — what other systems see (command-line, configuration, monitoring).
Both share the same core principle: design for the consumer, not the producer.
flowchart TD
INTF["🖥️ Interface Design"]
INTF --> UI["👤 User Interfaces\nWhat humans see and use"]
INTF --> SI["⚙️ System Interfaces\nWhat systems and operators use"]
UI --> UX["Consistency · Clarity · Feedback\nNavigation · Error states · Accessibility"]
SI --> SX["APIs · CLIs · Config · Monitoring\nLogs · Metrics · Health checks"]
Consistency is the multiplier: If every component has a different error format, debugging is painful. If every component has a different logging structure, observability is impossible. If every API has a different authentication pattern, integration is fragile.
Interface consistency checklist: Error handling — do all components return errors in the same format? Authentication — is auth handled consistently across all interfaces? Logging — do all components log in the same structured format? Monitoring — do all components expose health checks, metrics, and traces? Configuration — is configuration managed the same way across components? Naming — do URLs, fields, and parameters follow the same conventions?
Design system principles for UI consistency: Shared component library — buttons, forms, tables look and behave the same. Design tokens — colours, spacing, typography defined once, used everywhere. Interaction patterns — how errors are shown, how loading states work, how navigation flows. Accessibility — WCAG compliance as a non-negotiable standard.
Interface design isn't about aesthetics — it's about making the system comprehensible and usable for its consumers.
Try it yourself — The consistency check
How consistent are your interfaces?
| Dimension | Consistent? | Evidence |
|---|---|---|
| Error handling | ||
| Authentication | ||
| Logging | ||
| Monitoring | ||
| Configuration | ||
| Naming |
If fewer than four are consistent, your interfaces are creating friction.
6. Data design — Ownership, flow, transformation, and consistency
Data is the most coupling-prone part of any system. Get data ownership wrong and every boundary becomes an illusion.
Data design answers four questions: Who owns the data? — which component is the source of truth for each piece of data. How does data flow? — what's the path data takes between components. How is data transformed? — what happens to data as it moves between contexts. What consistency model applies? — strong, eventual, or causal consistency.
flowchart LR
SRC["📥 Source\nProduces data"] --> FLOW["🔄 Flow\nMoves through system"]
FLOW --> TRANS["🔧 Transform\nConverts format/schema"]
TRANS --> OWN["👑 Owner\nSource of truth"]
OWN --> CONSISTENCY["⚖️ Consistency\nStrong · Eventual · Causal"]
The golden rule: each piece of data has exactly one owner. Other components can have copies, but the owner is the source of truth. If two components can write the same data independently, the boundary is broken.
Data ownership patterns: Private data store — each component has its own database — no sharing. Maximum isolation — but data duplication and eventual consistency. Shared database (read) — components share a database for reading only. Simple queries — but read coupling. Event-sourced — events are the source of truth; state is reconstructed. Full audit trail — but complexity. Data mesh — domain teams own their data products as first-class assets. Scalable ownership — but requires platform investment.
Consistency models: Strong — every read sees the latest write. Latency, availability sacrifice. Use for financial transactions, inventory counts. Eventual — all replicas converge eventually. Temporary inconsistency is possible. Use for social feeds, product catalogues, analytics. Causal — related operations are ordered. More complex than eventual. Use for chat messages, collaborative editing.
Data flow patterns: Source → ETL/ELT → Warehouse (batch, analytics). Source → CDC → Stream → Consumer (real-time, event-driven). Source → API → Consumer (on-demand, synchronous). Source → Event → Multiple Consumers (pub/sub, reactive).
Data ownership is the foundation of every other boundary decision. Get this wrong and nothing else matters. One owner per data element — if two components can write it, the decomposition is wrong.
Try it yourself — The ownership check
Take three data elements in your system. Who owns them?
| Data element | Owner | Can others write? | If yes, boundary broken? |
|---|---|---|---|
If others can write, your boundary is broken. Fix it.
7. Process design — Mapping business workflows to system behaviour
A system isn't just a set of components — it implements business processes. If the system behaviour doesn't match the business workflow, the architecture may be internally clean but functionally broken.
Process design connects business workflows to system behaviour. It answers: "When a customer places an order, what happens in what order across which components?"
Key decisions: Orchestration vs choreography — one component coordinates, or each component reacts independently. Compensation and rollback — what happens when a step fails halfway through. Long-running processes — how to handle workflows that take minutes, hours, or days. Human-in-the-loop — where the process requires human judgement or approval.
flowchart TD
PROC["⚙️ Process Design"]
PROC --> ORCH["🎯 Orchestration\nCentral coordinator\ndirects the flow"]
PROC --> CHORE["💃 Choreography\nEach component reacts\nto events independently"]
ORCH --> ORCHPROS["Clear flow control\nEasy to monitor\nCentral point of failure"]
CHORE --> CHOREPROS["Loose coupling\nNo single point of failure\nHarder to trace"]
PROC --> FAIL["❌ Failure handling\nCompensation · Retry · Rollback"]
PROC --> LONG["⏳ Long-running\nSagas · State machines\nHuman approval gates"]
Orchestration vs choreography: Orchestration — central coordinator, clear flow control, easy to monitor, central point of failure. Best for complex, sequential workflows. Choreography — distributed reactions, loose coupling, no single point of failure, harder to trace. Best for simple, reactive workflows.
Saga pattern for long-running processes: A saga breaks a long transaction into a sequence of local transactions, each with a compensating action. Step 1: Reserve inventory → Compensating: Release inventory. Step 2: Charge payment → Compensating: Refund payment. Step 3: Create shipment → Compensating: Cancel shipment. If Step 3 fails → execute compensating actions for Step 2, then Step 1.
Process design is where architecture meets business reality — a clean component model that can't implement the business process isn't a solution.
Try it yourself — The process check
Take your most critical business process. How is it implemented?
| Question | Your answer |
|---|---|
| Orchestration or choreography? | |
| What happens when a step fails? | |
| Is it traceable end-to-end? | |
| Where are the human-in-the-loop points? |
If you can't answer the third question, your process isn't observable.
8. Putting it together — How these layers interact in a real solution
These six layers aren't independent. Choosing an event-driven integration pattern constrains your API design. Data ownership decisions constrain integration. Process design ties everything together.
In a real solution, the layers stack and interact:
flowchart TD
PROC["⚙️ 6. Process Design\nBusiness workflow → system behaviour"]
PROC --> DATA["📊 5. Data Design\nWho owns what, how it flows"]
PROC --> INTF["🖥️ 4. Interface Design\nWhat users and systems see"]
DATA --> API["🤝 3. API Design\nContracts between components"]
INTF --> API
API --> INT["🔄 2. Integration Patterns\nHow components communicate"]
INT --> APP["🧩 1. Application Design\nInternal structure of each component"]
A practical example — "Place Order" workflow: Process design — the workflow: validate → reserve inventory → charge payment → create shipment → notify customer. Data design — Order data owned by Ordering context, Payment data owned by Payments context, Inventory owned by Catalogue. Interface design — customer sees order confirmation page; operations team sees order management dashboard. API design — Ordering exposes POST /orders; Payments exposes POST /payments; Catalogue exposes POST /inventory/reserve. Integration — Ordering → Payments is asynchronous (payment may take time); Ordering → Catalogue is synchronous (inventory check is fast); Payment confirmation is an event published to the bus. Application design — Ordering service internally has: order validation layer, order state machine, order repository.
Decision dependency graph: First decide: Business process → Then decide: Integration pattern. Because process steps determine sync vs async needs. First decide: Data ownership → Then decide: API design. Because APIs expose owned data, not other's data. First decide: Integration pattern → Then decide: API style. Because sync → REST/gRPC, async → messaging, events → pub/sub. First decide: Consistency model → Then decide: Data flow. Because strong consistency → synchronous; eventual → async/events. First decide: Process complexity → Then decide: Orchestration vs choreography. Because complex sequential → orchestrate; simple reactive → choreograph.
The layers aren't a checklist to fill in order — they're a set of decisions that must be coherent across the whole solution. Solution design layers constrain each other — choose integration before API, data ownership before integration, process before everything.
Try it yourself — The coherence check
Take one real workflow. Run it through all six layers:
| Layer | Your answer |
|---|---|
| Process design | |
| Data design | |
| Interface design | |
| API design | |
| Integration | |
| Application design |
If the layers don't constrain each other coherently, you have design debt.
Putting it all together
Here's the complete picture in one diagram. This is the mental model worth internalising.
flowchart TD
BIZ["🏢 Business Process\nWhat the organisation does"]
BIZ --> SIX["📐 Six Design Layers"]
SIX --> A["🧩 Application\nWhat each part does"]
SIX --> I["🔄 Integration\nHow parts communicate"]
SIX --> C["🤝 API\nWhat promises parts make"]
SIX --> U["🖥️ Interface\nWhat people and systems see"]
SIX --> D["📊 Data\nWho owns what"]
SIX --> P["⚙️ Process\nHow workflows run"]
A --> SOL["✅ Real Solution\nComponents that work together\nData that flows correctly\nProcesses that complete"]
I --> SOL
C --> SOL
U --> SOL
D --> SOL
P --> SOL
The foundation:
Solution design has six layers — and they constrain each other. Process design determines integration patterns. Data ownership determines API design. Get the dependency order wrong and you redesign everything. Design for the consumer at every layer. Application design for the team that maintains it. API design for the component that calls it. Interface design for the person or system that uses it. Data ownership is the foundation. If two components can write the same data, every other design decision is built on sand. One owner per data element — always.
Cheat Sheet — All the key terms
| Layer | One-Line Memory | Key Decision |
|---|---|---|
| Application design | What each component does inside | Internal structure and responsibility scope |
| Integration patterns | How components communicate | Sync, async, event-driven, or batch |
| API design | The contract between components | Style, versioning, error handling |
| Interface design | What consumers see and use | Consistency across UI and system interfaces |
| Data design | Who owns what, how it flows | Ownership, flow, transformation, consistency |
| Process design | How business workflows run | Orchestration vs choreography, failure handling |
| REST | Resource-based, stateless | Web APIs, CRUD operations |
| GraphQL | Flexible queries | Mobile clients, complex data needs |
| gRPC | Fast, typed | Internal service-to-service |
| Event-driven | Pub/sub, decoupled | Reactive, multi-consumer |
| Saga | Long-running, compensated | Multi-step business transactions |
How to know if this landed
You'll know this has landed when someone stops designing components in isolation and starts asking "how do these layers constrain each other?" Each component has a clear, documented responsibility — no ambiguity about what it does. Integration patterns are chosen deliberately — not defaulted to REST for everything. APIs are versioned and documented — consumers know what to expect and when it changes. Data has exactly one owner per element — no shared write access across components. Business processes are traceable through the system — you can follow an order end-to-end. Interface consistency is enforced — errors, logging, auth follow the same patterns everywhere. And failure handling is designed in — compensation, retries, and dead letter queues exist from day one.
What changes when the mental model clicks
I've run this session with an insurance company where claims processing was a monolith — every step (registration, assessment, approval, payment, notification) was in one codebase. A change to the payment flow required regression testing the entire claims process. Data was shared across all modules — a schema change in payments broke the assessment dashboard. The gap at the start is usually not about understanding layers — it's about not seeing how they constrain each other.
What changes after this session:
Teams stop designing layers in isolation and start asking "how does this constrain the other layers?" The data ownership exercise — "who owns this data?" — is always the moment things click. People stop treating integration as a default-to-REST exercise and start treating it as a deliberate pattern choice. Their results get better. They stop building boundaries that two components can write through when the real problem was that they hadn't defined ownership.
The process design exercise tends to immediately change how teams think about their workflows. They start mapping business processes to system behaviour. Their traceability goes up. They stop building clean component models that can't implement the business process when the real problem was that they hadn't connected architecture to reality.
Book a Workshop
Ready to design the layers that make your architecture real?
or
2-day workshop includes application design exercise — structure your components internally, integration pattern selection — choose sync, async, event, or batch for your real scenarios, API design standards — create versioned, documented contracts, data ownership mapping — assign one owner per data element, process design workshop — map your business workflows to system behaviour, full-stack design exercise — connect all six layers for one real workflow, and integration design playbook + API design standards + data movement pattern guide.