Back
In print settings: Save as PDF, turn headers and footers off, turn background graphics on.

Software Architecture

A senior-level course on architectural decisions, quality attributes, boundaries, distributed systems, data, resilience, security, governance, and evolutionary design.

Official Documentation

June 2026

Contents

Foundations

  • Architecture as Decisions
  • Quality Attributes & Tradeoffs
  • Drivers, Constraints & Context

Design Analysis

  • Boundaries, Domains & Ownership
  • Styles, Tactics & Structural Patterns

Structure

  • Modular Monoliths & Internal Architecture

Distributed Design

  • Distributed Topologies & Service Decomposition
  • Integration, Contracts & Coupling
  • Data Architecture & Consistency

Cross-Cutting Qualities

  • Runtime Architecture & Resilience
  • Security, Privacy & Trust Boundaries
  • Platform, Deployment & Operations

Evolution

  • Observability & Architecture Fitness
  • Evolutionary Architecture & Governance
  • Architecture Review & Case Studies

Foundations

Section Detail

Architecture as Decisions

Architecture as Decisions

Software architecture is not the diagram. ISO/IEC/IEEE 42010 frames architecture around fundamental concepts, relationships, principles, and their environment; the practical consequence is that architecture lives in decisions, constraints, and stakeholder concerns. A senior architect is therefore a decision steward: discovering forces, making tradeoffs explicit, creating shared vocabulary, and preserving optionality where the future is uncertain.

The most common architecture failure is not picking the wrong technology. It is allowing important choices to remain implicit until they are too expensive to challenge. A team “just” adds a synchronous call, “just” shares a database table, “just” routes all traffic through one service, or “just” accepts a vendor default. Months later, performance, ownership, deployability, and compliance are shaped by those small choices. Architecture work turns these choices into visible design material that can be reviewed against stakeholder concerns.

Code
left to right direction
rectangle "Business Goal" as Goal
rectangle "Architectural Decision" as Decision
rectangle "System Constraint" as Constraint
rectangle "Team Behavior" as Behavior
rectangle "Runtime Outcome" as Outcome
rectangle "Feedback Signal" as Feedback

Goal --> Decision : creates pressure for
Decision --> Constraint : establishes
Constraint --> Behavior : guides
Behavior --> Outcome : produces
Outcome --> Feedback : emits
Feedback --> Decision : refines future choices
Business GoalArchitectural DecisionSystem ConstraintTeam BehaviorRuntime OutcomeFeedback Signalcreates pressure forestablishesguidesproducesemitsrefines future choices

Decisions Have Scope

Architectural decisions differ from implementation details because they affect multiple future choices. Choosing a database is not automatically architecture; choosing a consistency model that shapes product behavior, integration style, operational recovery, and team ownership is. Choosing a web framework may be local; choosing server-side rendering because search, accessibility, and content governance dominate the product strategy may be architectural.

The useful question is not “is this architecture?” but “what becomes harder or easier because of this choice?” If a choice changes deployability, testability, data ownership, security boundaries, cost behavior, or team topology, treat it as architecture. The decision deserves a rationale, expected consequences, and a way to revisit it.

Code
left to right direction
rectangle "Local Choice" as Local
rectangle "Architectural Choice" as Arch
rectangle "Single Module" as Module
rectangle "Multiple Teams" as Teams
rectangle "Runtime Qualities" as Qualities
rectangle "Operating Model" as Ops

Local --> Module : mostly affects
Arch --> Teams : coordinates
Arch --> Qualities : constrains
Arch --> Ops : shapes
Teams --> Qualities : delivery behavior changes runtime behavior
Ops --> Qualities : incident and cost patterns expose design
Local ChoiceArchitectural ChoiceSingle ModuleMultiple TeamsRuntime QualitiesOperating Modelmostly affectscoordinatesconstrainsshapesdelivery behavior changesruntime behaviorincident and cost patternsexpose design

The Decision Record

An architecture decision record is valuable because it preserves the thinking, not because it creates documentation. The minimum useful record contains: context, decision, alternatives considered, consequences, and review trigger. The review trigger is often omitted, yet it is the piece that keeps the record alive. A decision without a review trigger quietly becomes doctrine.

A good decision record should be short enough to read during a design review and precise enough to prevent repeated arguments. “Use Kafka” is weak. “Use Kafka as the durable event backbone for fulfillment events because downstream consumers need replay, independent delivery, and delayed adoption; review if end-to-end latency must drop below 200 ms or if consumer count remains below three for two quarters” is stronger because it records context, decision, consequences, and the conditions under which the decision should be challenged.

Code
rectangle "ADR: Async Fulfillment Events" as ADR {
rectangle "Context\nOrder placement must not wait for warehouse, invoicing, and analytics." as Context
rectangle "Decision\nPublish immutable fulfillment events to a durable log." as Decision
rectangle "Alternatives\nDirect calls, shared database, nightly export." as Alternatives
rectangle "Consequences\nReplay and loose coupling; schema governance and eventual consistency." as Consequences
rectangle "Review Trigger\nRevisit if latency target or consumer count assumptions change." as Trigger
}
Context --> Decision
Alternatives --> Decision
Decision --> Consequences
Consequences --> Trigger
ADR: Async Fulfillment EventsContextOrder placement must not waitfor warehouse, invoicing, andanalytics.DecisionPublish immutable fulfillmentevents to a durable log.AlternativesDirect calls, shared database,nightly export.ConsequencesReplay and loose coupling;schema governance andeventual consistency.Review TriggerRevisit if latency target orconsumer count assumptionschange.

Tradeoffs, Not Truths

Senior architecture work replaces abstract best practices with explicit tradeoffs. “Microservices are scalable” is too vague to be useful. A distributed design may scale team autonomy and independent deploys while reducing local reasoning, increasing operational load, and making data consistency a product concern. A modular monolith may simplify debugging and transactions while requiring strong internal boundaries and disciplined ownership.

Every serious decision has a bill. The question is whether the bill is paid in a currency the organization can afford. A team with strong operations and weak domain clarity should not copy the same architecture as a team with mature product boundaries and weak release engineering. Architecture is local to the context, even when patterns have general names.

Code
left to right direction
rectangle "Decision" as D
rectangle "Benefits" as B
rectangle "Costs" as C
rectangle "Risks" as R
rectangle "Mitigations" as M
rectangle "Fitness Checks" as F

D --> B : buys
D --> C : pays
D --> R : exposes
R --> M : reduced by
M --> F : verified by
F --> D : keeps honest
DecisionBenefitsCostsRisksMitigationsFitness Checksbuyspaysexposesreduced byverified bykeeps honest

Feedback Loops

Architecture should be evaluated through feedback, not ceremony. The best feedback comes from running software: deployment frequency, lead time, incident patterns, user latency, queue depth, schema-change friction, recovery time, cost per transaction, escaped defects, and the time required for a new engineer to make a safe change. These signals reveal whether the architecture is serving the system or becoming a museum.

Reviews are still useful, but only when they connect design intent to observable behavior. A review that asks “does this match our reference diagram?” is weaker than one that asks “what quality attribute is this decision protecting, how will we know if it fails, and what option remains if our assumption is wrong?”

Practice

Take a system you know and identify five decisions that are currently implicit. For each, write one sentence for context, one sentence for the decision, one sentence for the consequence, and one sentence for the review trigger. Then mark which decisions are reversible, which are expensive but manageable, and which would require product or organizational redesign to change.

References & Further Reading

Section Detail

Quality Attributes & Tradeoffs

Quality Attributes & Tradeoffs

Quality attributes are the real language of architecture. SEI’s architecture-evaluation work treats qualities such as performance, modifiability, availability, and security as scenario-driven concerns, not as vague labels. Users rarely ask for “hexagonal architecture” or “event sourcing”; they ask for a service that is fast during campaigns, safe during fraud attempts, recoverable after mistakes, understandable by support, and cheap enough to operate.

Quality attributes are also in tension. Caching may improve latency while weakening freshness and increasing invalidation complexity. Strong consistency may simplify user promises while reducing availability during partitions. Fine-grained services may improve team autonomy while increasing runtime coordination. Security controls may reduce risk while adding latency and operational friction. Senior architecture is the discipline of naming these tensions before they become surprises.

Code
left to right direction
rectangle "Quality Attribute Scenario" as Scenario
rectangle "Stimulus\nWhat happens?" as Stimulus
rectangle "Environment\nUnder what conditions?" as Environment
rectangle "Response\nWhat should the system do?" as Response
rectangle "Measure\nHow good is good enough?" as Measure

Scenario --> Stimulus
Scenario --> Environment
Scenario --> Response
Scenario --> Measure
Stimulus --> Response : triggers
Environment --> Measure : qualifies
Quality Attribute ScenarioStimulusWhat happens?EnvironmentUnder what conditions?ResponseWhat should the system do?MeasureHow good is good enough?triggersqualifies

Scenarios Beat Adjectives

”The system must be scalable” is not an architectural requirement. It is a mood. A quality attribute scenario turns a mood into something designable: during a flash sale, with ten times normal traffic, checkout accepts orders at p95 latency below 400 ms while preserving payment correctness and shedding non-critical personalization.

The scenario has a stimulus, environment, response, and measure. It also has a business meaning. A reporting system may tolerate minutes of delay because correctness and auditability dominate. A trading system may accept complex infrastructure to reduce tail latency. A public health system may prefer graceful degradation and data integrity over feature richness. Quality attributes are not universal priorities; they are expressions of what failure would cost.

Code
rectangle "Checkout Flash Sale Scenario" as S {
rectangle "Stimulus\n10x traffic spike" as A
rectangle "Environment\nCampaign active, inventory constrained" as B
rectangle "Response\nAccept paid orders, defer recommendations" as C
rectangle "Measure\np95 checkout under 400 ms; no duplicate charges" as D
}
A --> C
B --> C
C --> D
Checkout Flash Sale ScenarioStimulus10x traffic spikeEnvironmentCampaign active, inventoryconstrainedResponseAccept paid orders, deferrecommendationsMeasurep95 checkout under 400 ms; noduplicate charges

Tactics Are Smaller Than Patterns

Patterns name larger arrangements. Tactics are smaller design moves that influence a quality attribute. For availability, tactics include redundancy, heartbeat, failover, circuit breaking, health checks, and graceful degradation. For modifiability, tactics include information hiding, stable interfaces, dependency inversion, module ownership, and automated compatibility tests. For performance, tactics include caching, batching, asynchronous processing, indexing, precomputation, partitioning, and admission control.

The distinction matters because architects often debate patterns when the real work is choosing tactics. You do not adopt microservices to get availability. You apply redundancy, isolation, deployment independence, and operational practices that may or may not require services. You do not adopt domain-driven design to get modifiability. You create boundaries, language, ownership, and dependency rules that let a domain change without dragging unrelated concepts behind it.

Code
left to right direction
rectangle "Availability" as Availability
rectangle "Modifiability" as Modifiability
rectangle "Performance" as Performance

rectangle "Redundancy" as Redundancy
rectangle "Graceful Degradation" as Degradation
rectangle "Stable Interfaces" as Interfaces
rectangle "Dependency Rules" as Dependency
rectangle "Caching" as Caching
rectangle "Admission Control" as Admission

Availability --> Redundancy
Availability --> Degradation
Modifiability --> Interfaces
Modifiability --> Dependency
Performance --> Caching
Performance --> Admission
AvailabilityModifiabilityPerformanceRedundancyGraceful DegradationStable InterfacesDependency RulesCachingAdmission Control

Tradeoff Surfaces

A tradeoff surface is the area where one quality improves at the expense of another. Senior architects make the surface visible so stakeholders can choose knowingly. For example, a product team may ask for real-time dashboards. The architecture choices include direct reads from the transactional database, replicas, event streams, materialized views, or a separate analytical store. Each choice expresses a tradeoff among freshness, load isolation, correctness, cost, and implementation effort.

When tradeoffs stay technical, decisions drift. The business may say “real time” but mean “fresh enough for a manager to notice a bad campaign within five minutes.” That difference changes the architecture. A five-minute tolerance may avoid a fragile live query path and allow a resilient materialized view. Architecture quality improves when technical choices are linked to business tolerances.

Code
left to right direction
database "Transactional DB" as OLTP
queue "Event Stream" as Stream
database "Materialized View" as View
rectangle "Dashboard" as Dash
rectangle "Tradeoff\nFreshness: seconds to minutes\nIsolation: high\nComplexity: moderate" as Tradeoff

OLTP --> Stream : domain events
Stream --> View : projection
View --> Dash : query
Tradeoff .. Dash
Transactional DBEvent StreamMaterialized ViewDashboardTradeoffFreshness: seconds to minutesIsolation: highComplexity: moderatedomain eventsprojectionquery

Prioritization Under Scarcity

Most systems cannot maximize every quality. The architecture should state the top qualities and the qualities that are intentionally secondary. This is not negligence; it is honesty. A prototype may optimize learning and reversibility over scale. A regulated financial platform may optimize auditability and correctness over speed of feature variation. A media site may optimize read latency and cost over strict consistency for comments.

Prioritization should be revisited as the system matures. Early systems often need modifiability and fast learning. Growing systems need operability and reliability. Mature systems need governance, migration paths, and cost control. The same architecture that helps a team learn may become dangerous when traffic, compliance, or dependency count grows.

Practice

Write three quality attribute scenarios for one product: one user-facing, one operational, and one change-oriented. For each scenario, list two tactics that improve it and one quality attribute that could get worse. Then decide which tradeoff you would accept now and which metric would tell you that the decision needs review.

References & Further Reading

Section Detail

Drivers, Constraints & Context

Drivers, Constraints & Context

Architecture begins before technology selection. ISO 42010 emphasizes stakeholders, concerns, and environment because the strongest architectural forces often come from outside the codebase: revenue model, customer trust, regulatory exposure, staffing, procurement, legacy integration, data residency, existing contracts, and the organization’s appetite for operational complexity. A design that ignores context may look elegant while being impossible to staff, certify, migrate, or afford.

Senior architects separate drivers from constraints. A driver is a force that makes a quality important: growth, risk, product strategy, market timing, or operational pain. A constraint is a boundary the solution must respect: approved cloud provider, data residency, existing ERP, contractual latency, team skill, budget, or migration deadline. Constraints are not excuses. They are design inputs.

Code
left to right direction
rectangle "Business Drivers" as Drivers
rectangle "System Constraints" as Constraints
rectangle "Architectural Options" as Options
rectangle "Decision" as Decision
rectangle "Risk Register" as Risk
rectangle "Roadmap" as Roadmap

Drivers --> Options : prioritize
Constraints --> Options : limit
Options --> Decision : evaluated into
Decision --> Risk : creates residual
Decision --> Roadmap : sequences
Risk --> Roadmap : mitigation work
Business DriversSystem ConstraintsArchitectural OptionsDecisionRisk RegisterRoadmapprioritizelimitevaluated intocreates residualsequencesmitigation work

Context Mapping

A context map identifies the systems, teams, vendors, users, and policies that shape the architecture. The goal is not to draw every integration. The goal is to understand where autonomy ends. A billing service depends on payment providers, accounting rules, tax engines, customer identity, dispute operations, and support workflows. Its architecture is partly a social contract among those parties.

Context maps reveal hidden coupling. If a team believes it owns invoicing but every invoice field is dictated by finance operations and the ERP, the boundary is not as autonomous as the code suggests. If customer support needs immediate correction of failed orders, an asynchronous pipeline must include human recovery paths, not merely retry logic. Architecture is only real when it includes the people and institutions that operate it.

Code
left to right direction
actor "Customer" as Customer
actor "Support Agent" as Support
rectangle "Commerce Platform" as Commerce
rectangle "Billing Context" as Billing
rectangle "ERP" as ERP
rectangle "Tax Provider" as Tax
rectangle "Payment Provider" as Payment
rectangle "Compliance Policy" as Policy

Customer --> Commerce : buys
Commerce --> Billing : order to invoice
Billing --> Payment : capture and refund
Billing --> Tax : calculate tax
Billing --> ERP : post ledger entries
Support --> Billing : correct exceptions
Policy .. Billing : constrains retention and audit
CustomerSupport AgentCommerce PlatformBilling ContextERPTax ProviderPayment ProviderCompliance Policybuysorder to invoicecapture and refundcalculate taxpost ledger entriescorrect exceptionsconstrains retention and audit

Architectural Drivers

Drivers should be expressed as pressure, not slogans. “International expansion” becomes data residency, localization, regional latency, tax calculation, support coverage, and identity-provider variation. “Enterprise readiness” becomes audit logs, role-based access, SSO, tenant isolation, contract-specific configuration, and migration tooling. “Developer velocity” becomes local development speed, test determinism, dependency management, deploy confidence, and cognitive load.

Each driver should map to one or more architectural implications. This keeps strategy connected to design. If the driver has no implication, it is probably not a driver. If the implication has no driver, it may be preference disguised as architecture.

Code
rectangle "International Expansion" as Expansion
rectangle "Architectural Implications" as Implications {
rectangle "Regional data storage" as Data
rectangle "Configurable compliance rules" as Rules
rectangle "Provider abstraction for tax and payments" as Providers
rectangle "Localized read models" as LocalRead
}
Expansion --> Data
Expansion --> Rules
Expansion --> Providers
Expansion --> LocalRead
Architectural ImplicationsRegional data storageConfigurable compliance rulesProvider abstraction for tax andpaymentsLocalized read modelsInternational Expansion

Constraints as Design Material

Constraints can sharpen design. A small team constraint may rule out a fleet of independently deployed services and favor a modular monolith with strong internal boundaries. A data residency constraint may require regional storage and event filtering. A legacy dependency constraint may motivate an anti-corruption layer rather than allowing old data shapes to leak through the new system.

The architect’s job is to distinguish hard constraints from inherited assumptions. “We must use the existing database” may be contractual, financial, political, or merely convenient. Each version leads to different design. Hard constraints need adaptation. Soft constraints can be challenged with evidence and staged migration.

Code
left to right direction
rectangle "Constraint" as Constraint
rectangle "Decision\nHard constraint?" as Hard
rectangle "Adapt Architecture" as Adapt
rectangle "Challenge Assumption" as Challenge
rectangle "Experiment or Spike" as Spike
rectangle "Decision Record" as ADR

Constraint --> Hard
Hard --> Adapt : yes
Hard --> Challenge : no or unclear
Challenge --> Spike
Spike --> ADR
Adapt --> ADR
ConstraintDecisionHard constraint?Adapt ArchitectureChallenge AssumptionExperiment or SpikeDecision Recordyesno or unclear

Fitness to Organization

Architecture and organization co-evolve. A team cannot sustainably operate an architecture that requires capabilities it does not have: observability, incident response, schema governance, security review, release automation, cost analysis, or domain ownership. Conway’s Law is not just a warning that systems mirror communication structures. It is also a lever: changing team boundaries, ownership, and communication pathways can change the architecture that emerges.

Senior-level architecture therefore includes operating model design. Who owns production? Who approves schema changes? Who can deploy independently? Who defines contract compatibility? Who handles cross-cutting concerns like identity, observability, and platform standards? If the answers are vague, the technical design will eventually encode accidental governance.

Practice

Choose a product initiative and list ten contextual facts before proposing architecture. Mark each fact as driver, hard constraint, soft constraint, or unknown. Then write three design implications and one organizational implication. The aim is to make the architecture emerge from reality rather than from a favorite pattern.

References & Further Reading

Design Analysis

Section Detail

Boundaries, Domains & Ownership

Boundaries, Domains & Ownership

Boundaries are the most important architectural material. Parnas’s information-hiding argument, domain-driven design’s bounded contexts, and modern service-ownership practice all point to the same architectural pressure: hide unstable decisions behind a boundary that has a clear language and owner. Bad boundaries make every change cross-cutting. Good boundaries turn complexity into smaller local problems with explicit integration points.

Domain boundaries are not found by looking at tables or controllers. They are found by listening to language, responsibility, cadence of change, invariants, and business capability. “Customer” may mean a marketing lead, an authenticated user, a legal contracting party, a billing account, or a support contact. If one model tries to satisfy all meanings, the architecture becomes ambiguous. Separate models can be integrated; confused models corrupt each other.

Code
left to right direction
rectangle "Identity Context" as Identity {
rectangle "User\nlogin, credentials, sessions" as User
}
rectangle "Sales Context" as Sales {
rectangle "Prospect\nlead stage, account owner" as Prospect
}
rectangle "Billing Context" as Billing {
rectangle "Account\nlegal entity, invoice terms" as Account
}
rectangle "Support Context" as Support {
rectangle "Contact\ncase history, permissions" as Contact
}

Identity --> Sales : user profile signal
Sales --> Billing : won contract
Billing --> Support : entitlement status
Support --> Identity : access verification
Identity ContextSales ContextBilling ContextSupport ContextUserlogin, credentials, sessionsProspectlead stage, account ownerAccountlegal entity, invoice termsContactcase history, permissionsuser profile signalwon contractentitlement statusaccess verification

Cohesion and Change

A useful boundary groups things that change together and separates things that change for different reasons. Cohesion is not about placing similar nouns together; it is about shared rules. Order pricing, promotion eligibility, tax calculation, inventory reservation, and payment capture may all appear in checkout, but they do not necessarily belong to one module. The question is which rules must be consistent at the same moment and which can evolve independently.

Change history is a strong boundary signal. If compliance rules change monthly while catalog browsing changes weekly and payment integration changes only when providers evolve, forcing them through one release path creates friction. Conversely, splitting one invariant across three services may create distributed transaction pain for no real autonomy. Boundaries should respect both domain meaning and change cadence.

Code
rectangle "Change Patterns" as Change
rectangle "High Cohesion Boundary" as Boundary {
rectangle "Rules change together" as R1
rectangle "Same owner can explain them" as R2
rectangle "State transitions share invariants" as R3
}
rectangle "Separate Boundary" as Separate {
rectangle "Different vocabulary" as S1
rectangle "Different release cadence" as S2
rectangle "Different failure tolerance" as S3
}
Change --> Boundary
Change --> Separate
High Cohesion BoundarySeparate BoundaryRules change togetherSame owner can explain themState transitions share invariantsDifferent vocabularyDifferent release cadenceDifferent failure toleranceChange Patterns

Data Ownership

Data ownership is often where architectural ideals meet reality. If multiple services write the same table, ownership is fictional. If reporting jobs bypass domain APIs and mutate operational state, invariants are unprotected. If every team reads every table directly, schema changes become organization-wide events.

Owning data does not mean hiding all data. It means owning the rules that make the data valid. Other contexts may receive events, query read models, or use published APIs. They should not quietly depend on private storage structures. A private table is an implementation detail; a contract is an architectural commitment.

Code
left to right direction
rectangle "Order Context" as Order {
database "Private Order Store" as OrderDB
rectangle "Order API" as OrderAPI
queue "Order Events" as Events
}
rectangle "Fulfillment Context" as Fulfillment {
database "Fulfillment Store" as FulfillDB
}
rectangle "Analytics Context" as Analytics {
database "Analytical Model" as AnalyticsDB
}

OrderAPI --> OrderDB : owns writes
OrderAPI --> Events : publishes facts
Events --> Fulfillment : consumes
Events --> Analytics : projects
Fulfillment -[hidden]- Analytics
Order ContextFulfillment ContextAnalytics ContextPrivate Order StoreOrder APIOrder EventsFulfillment StoreAnalytical Modelowns writespublishes factsconsumesprojects

Boundary Interfaces

Every boundary needs an interface, and not all interfaces are APIs. An interface may be a synchronous endpoint, event stream, file export, shared library, command queue, admin workflow, data product, or human approval process. The architectural question is what the interface promises and what it hides.

Stable interfaces should expose business capabilities rather than internal data shapes. “Create shipment for paid order” is a stronger capability boundary than “insert row into shipments.” “Customer credit changed” is a stronger event than “customer table updated.” Capability-oriented interfaces are easier to evolve because they preserve intent while allowing implementation changes.

Code
left to right direction
rectangle "Capability Interface" as Capability
rectangle "Command\nrequest action" as Command
rectangle "Event\nannounce fact" as Event
rectangle "Query\nread published view" as Query
rectangle "Policy\nhuman or automated rule" as Policy

Capability --> Command
Capability --> Event
Capability --> Query
Capability --> Policy
Capability InterfaceCommandrequest actionEventannounce factQueryread published viewPolicyhuman or automated rule

Ownership and Team Topology

A boundary without ownership decays. Someone must be accountable for the language, contracts, quality attributes, and runtime behavior inside the boundary. Shared ownership can work for libraries or platforms, but product domains usually need clear owners. Otherwise every team optimizes locally and the domain model becomes a negotiation artifact.

Team topology should follow cognitive load. A stream-aligned team should own a coherent product or domain slice. A platform team should reduce operational and delivery burden without taking ownership away from product teams. An enabling team should teach and accelerate adoption of practices. Complicated subsystem teams should exist only where deep specialist knowledge is genuinely required.

Practice

Pick a messy domain noun such as customer, account, order, product, or subscription. Write at least four meanings used by different stakeholders. Then propose bounded contexts, name the owner of each context, identify the private data each owns, and define one public contract for integration. If the same field appears in multiple contexts, state whether it is copied, derived, or independently meaningful.

References & Further Reading

  • David L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules” (Communications of the ACM, standard copyright)
  • Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (Addison-Wesley, standard copyright)
  • Building Microservices by Sam Newman (O’Reilly, standard copyright)
  • Microservices Patterns by Chris Richardson (Manning, standard copyright)
Section Detail

Styles, Tactics & Structural Patterns

Styles, Tactics & Structural Patterns

Architectural styles are reusable structural answers to recurring forces. The Azure Architecture Center presents styles such as layered, microservices, event-driven, and CQRS as choices with known benefits and liabilities, not as maturity levels. Layered architecture manages dependency direction and separation of concerns. Hexagonal architecture protects the domain from infrastructure details. Event-driven architecture decouples producers from consumers in time. Microservices align deployable units with ownership boundaries.

The mistake is treating styles as identities. A system is rarely “a microservices architecture” in a pure sense. It may use a modular monolith for core transactions, event-driven integration for downstream workflows, a data lake for analytics, and serverless functions for low-risk automation. Senior architects compose styles deliberately and explain which forces each style addresses.

Code
left to right direction
rectangle "Architectural Forces" as Forces
rectangle "Layered" as Layered
rectangle "Hexagonal" as Hex
rectangle "Event Driven" as Event
rectangle "Microservices" as Micro
rectangle "Pipeline" as Pipe
rectangle "Composed System" as System

Forces --> Layered : dependency control
Forces --> Hex : domain isolation
Forces --> Event : temporal decoupling
Forces --> Micro : team autonomy
Forces --> Pipe : transformation flow
Layered --> System
Hex --> System
Event --> System
Micro --> System
Pipe --> System
Architectural ForcesLayeredHexagonalEvent DrivenMicroservicesPipelineComposed Systemdependency controldomain isolationtemporal decouplingteam autonomytransformation flow

Layered and Hexagonal Thinking

Layered architecture is useful when dependency direction matters: interface layer calls application services, application services coordinate domain behavior, domain rules avoid infrastructure dependencies, and infrastructure implements persistence, messaging, and external adapters. The risk is that layers become pass-through bureaucracy or that domain logic leaks into controllers and repositories.

Hexagonal architecture sharpens the dependency rule by putting the domain and application core at the center. Ports define what the core needs or offers. Adapters translate between external technologies and those ports. This style is valuable when domain behavior must survive framework changes, external providers, or testing needs. It is less valuable when the system is mostly CRUD with little domain complexity; then strict ports may become ceremony.

Code
left to right direction
rectangle "Driving Adapters" as Driving {
rectangle "Web UI" as Web
rectangle "CLI" as CLI
}
rectangle "Application Core" as Core {
rectangle "Use Cases" as UseCases
rectangle "Domain Model" as Domain
rectangle "Ports" as Ports
}
rectangle "Driven Adapters" as Driven {
database "Database" as DB
queue "Message Broker" as Broker
rectangle "Payment Provider" as Pay
}

Web --> UseCases
CLI --> UseCases
UseCases --> Domain
UseCases --> Ports
Ports --> DB
Ports --> Broker
Ports --> Pay
Driving AdaptersApplication CoreDriven AdaptersWeb UICLIUse CasesDomain ModelPortsDatabaseMessage BrokerPayment Provider

Event-Driven Style

Event-driven architecture is powerful when facts need to be observed by multiple consumers, producers should not know every downstream workflow, or systems need replay and temporal decoupling. It changes the design vocabulary from “call this thing now” to “this fact happened.” That shift supports extensibility, but it also introduces schema governance, ordering concerns, idempotency, observability challenges, and eventual consistency.

Events should represent business facts, not database accidents. “OrderPaid” is meaningful. “PaymentRowUpdated” is a leaky implementation signal. Consumers should be able to interpret the event without depending on private producer internals. Producers should document event versioning, delivery guarantees, retention, and whether consumers may replay.

Code
left to right direction
rectangle "Order Service" as Order
queue "Event Log" as Log
rectangle "Fulfillment" as Fulfillment
rectangle "Invoicing" as Invoicing
rectangle "Customer Messaging" as Messaging
rectangle "Analytics Projection" as Analytics

Order --> Log : OrderPaid
Log --> Fulfillment : reserve and ship
Log --> Invoicing : create invoice
Log --> Messaging : send receipt
Log --> Analytics : update metrics
Order ServiceEvent LogFulfillmentInvoicingCustomer MessagingAnalytics ProjectionOrderPaidreserve and shipcreate invoicesend receiptupdate metrics

Service-Oriented and Microservice Styles

Microservices are not small classes over HTTP. They are independently deployable services aligned to business capabilities and owned by teams that can operate them. The main architectural benefit is not that services are small. It is that change, deployment, scaling, and failure can be isolated around meaningful boundaries.

The costs are real. Distributed systems require network-aware design, observability, versioned contracts, deployment automation, data ownership, incident response, and higher cognitive overhead. When these capabilities are weak, microservices can create more coordination than they remove. A senior architect asks whether the organization needs independent deployment enough to pay the operational price.

Code
left to right direction
rectangle "Capability Service" as Service {
rectangle "API Contract" as API
rectangle "Business Logic" as Logic
database "Owned Data" as Data
rectangle "Runbook and Alerts" as Ops
}
rectangle "Owning Team" as Team
rectangle "Consumers" as Consumers
rectangle "Platform" as Platform

Team --> Service : builds and operates
Consumers --> API : depend on contract
Logic --> Data : owns invariants
Service --> Platform : uses paved road
Ops --> Team : feedback
Capability ServiceAPI ContractBusiness LogicOwned DataRunbook and AlertsOwning TeamConsumersPlatformbuilds and operatesdepend on contractowns invariantsuses paved roadfeedback

Choosing and Composing Styles

Style choice should follow forces. If you need strong transactional consistency and a small team, a modular monolith may be superior. If you need multiple downstream reactions and audit replay, event-driven integration may be worth the governance overhead. If you need long-lived domain behavior insulated from infrastructure churn, ports and adapters can help. If you need independent scaling of a read-heavy capability, CQRS and materialized views may be relevant.

The best architecture descriptions often say “we use this style here, but not there.” That sentence is a sign of thoughtfulness. It avoids one-size-fits-all design and lets each part of the system pay only for the complexity it needs.

Practice

For a system you know, identify three different architectural forces. Propose one style or tactic for each force and name the cost it introduces. Then draw a hybrid architecture that uses at least two styles. The result should not be beautiful; it should be honest about where each style earns its keep.

References & Further Reading

Structure

Section Detail

Modular Monoliths & Internal Architecture

Modular Monoliths & Internal Architecture

A modular monolith is a single deployable system with explicit internal boundaries. It is not a polite name for a tangled codebase. It applies information hiding and bounded-context thinking inside one runtime so deployment, transactions, debugging, and local development stay simple while domain ownership remains visible. For many products, especially those built by small or medium teams, it is the most underused senior-level architecture.

The core idea is simple: pay for distribution only when you need distribution. A monolith can contain independent domains, ports, adapters, internal events, private persistence schemas, and strict dependency rules. It can also produce excellent operational behavior because there is one runtime, one deployable, one debugger path, and fewer network failure modes. The challenge is discipline. Without enforcement, a modular monolith tends to dissolve into shared utilities, direct table access, and cross-module shortcuts.

Code
left to right direction
rectangle "Single Deployable" as App {
rectangle "Catalog Module" as Catalog
rectangle "Ordering Module" as Ordering
rectangle "Billing Module" as Billing
rectangle "Support Module" as Support
rectangle "Shared Kernel\nsmall, stable, boring" as Kernel
}
database "Database\nseparate schemas or ownership rules" as DB

Catalog --> Kernel
Ordering --> Kernel
Billing --> Kernel
Support --> Kernel
Ordering --> Billing : published interface
Billing --> DB : owns billing tables
Ordering --> DB : owns order tables
Catalog --> DB : owns catalog tables
Single DeployableCatalog ModuleOrdering ModuleBilling ModuleSupport ModuleShared Kernelsmall, stable, boringDatabaseseparate schemas or ownershiprulespublished interfaceowns billing tablesowns order tablesowns catalog tables

Internal Boundaries

Internal boundaries need more than folders. They need dependency rules, public APIs, private implementation, test coverage, and review habits. A module should expose capabilities and hide its data structures. Other modules should call its public interface or consume its published events, not reach into its repositories or tables.

This is where language matters. A “service” inside a monolith can become a bag of methods unless it is attached to domain responsibility. “OrderPlacement” is clearer than “OrderService” if the capability is placing an order. “BillingAccount” is clearer than “CustomerEntity” if the context owns legal invoicing behavior. Internal architecture should help developers speak in the domain language, not merely organize technical layers.

Code
rectangle "Ordering Module" as Ordering {
rectangle "Public API\nplace order, cancel order, query order" as Public
rectangle "Application Logic" as AppLogic
rectangle "Domain Rules" as Domain
rectangle "Private Persistence" as Persistence
}
rectangle "Billing Module" as Billing
rectangle "Catalog Module" as Catalog

Billing --> Public : uses capability
Catalog --> Public : reads published view
Public --> AppLogic
AppLogic --> Domain
AppLogic --> Persistence
Billing -[#FF5555,dashed]-> Persistence : forbidden shortcut
Ordering ModulePublic APIplace order, cancel order, queryorderApplication LogicDomain RulesPrivate PersistenceBilling ModuleCatalog Moduleuses capabilityreads published viewforbidden shortcut

Dependency Rules

Dependency rules are the immune system of a modular monolith. Common rules include: domain modules may not depend on web frameworks, modules may not import each other’s private packages, shared code must be stable and small, infrastructure depends inward, and data access must go through owning modules. These rules should be automated with architecture tests or static analysis. Social discipline alone decays under deadline pressure.

The shared kernel deserves special suspicion. It should contain concepts that are genuinely stable across contexts: identifiers, money primitives, time abstractions, result types, or security principal shapes. It should not become the place where disputed domain concepts go to avoid ownership conversations. When everything is shared, nothing is owned.

Code
left to right direction
rectangle "Allowed Dependencies" as Allowed {
rectangle "Interface Layer" as Interface
rectangle "Application Layer" as Application
rectangle "Domain Layer" as Domain
rectangle "Infrastructure Layer" as Infrastructure
}
Interface --> Application
Application --> Domain
Infrastructure --> Application
Infrastructure --> Domain

rectangle "Forbidden" as Forbidden
Domain -[#FF5555,dashed]-> Infrastructure : no
Application -[#FF5555,dashed]-> Interface : no
rectangle "Other Module Private Code" as Private
Application -[#FF5555,dashed]-> Private : no
Allowed DependenciesInterface LayerApplication LayerDomain LayerInfrastructure LayerForbiddenOther Module Private Codenonono

Data Inside the Monolith

A modular monolith may use one physical database, but it should not treat all data as communal. Separate schemas, table naming, repository ownership, or database permissions can reinforce boundaries. The important rule is that one module owns the invariants for its data. Other modules can request actions, subscribe to events, or read published views.

Transactions are one of the advantages of a monolith, but they should be used deliberately. A single transaction across modules may be appropriate for a genuine invariant. It may also be a sign that the boundary is wrong or that the business process needs a saga-style workflow. The fact that a transaction is easy does not mean it is architecturally harmless.

Code
left to right direction
rectangle "Order Placement" as UseCase
rectangle "Ordering Module" as Ordering
rectangle "Inventory Module" as Inventory
rectangle "Billing Module" as Billing
database "Order Schema" as OrderDB
database "Inventory Schema" as InvDB
database "Billing Schema" as BillDB
queue "Internal Event Bus" as Bus

UseCase --> Ordering
Ordering --> OrderDB : owns
Ordering --> Inventory : reserve through API
Inventory --> InvDB : owns
Ordering --> Bus : OrderPlaced
Bus --> Billing : starts invoice workflow
Billing --> BillDB : owns
Order PlacementOrdering ModuleInventory ModuleBilling ModuleOrder SchemaInventory SchemaBilling SchemaInternal Event Busownsreserve through APIownsOrderPlacedstarts invoice workflowowns

Extractability

One promise of a modular monolith is that a module can later be extracted into a service. That promise is only credible if the module already owns its data, has a narrow public contract, avoids private imports from other modules, and can be tested independently. Extraction is not mainly about moving code into another repository. It is about replacing in-process calls with network calls, replacing local transactions with distributed workflows, and creating independent deployment and operations.

Designing for possible extraction does not mean prematurely building microservices. It means keeping seams honest. Use module APIs that could become remote without changing business semantics. Publish events that could move to a broker later. Keep data ownership clear. Track dependencies. These practices improve the monolith even if extraction never happens.

Code
left to right direction
rectangle "Before\nModular Monolith" as Before {
rectangle "Billing Module" as BillingIn
}
rectangle "Extraction Work" as Work {
rectangle "Replace in-process API with network contract" as C1
rectangle "Move owned schema or synchronize data" as C2
rectangle "Add deployment, observability, runbook" as C3
}
rectangle "After\nBilling Service" as After {
rectangle "Billing API" as API
database "Billing DB" as DB
}

Before --> Work
Work --> After
C1 --> API
C2 --> DB
C3 --> After
BeforeModular MonolithExtraction WorkAfterBilling ServiceBilling ModuleReplace in-process API withnetwork contractMove owned schema orsynchronize dataAdd deployment, observability,runbookBilling APIBilling DB

Practice

Take an existing monolith or imagine one. Define five modules, the owner of each module, its public interface, its private data, and one forbidden dependency. Then choose one module that might be extracted in the future and list what would have to become explicit before extraction is safe.

References & Further Reading

  • David L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules” (Communications of the ACM, standard copyright)
  • Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (Addison-Wesley, standard copyright)
  • Martin Fowler and James Lewis: Microservices (standard copyright)
  • Monolith to Microservices by Sam Newman (O’Reilly, standard copyright)

Distributed Design

Section Detail

Distributed Topologies & Service Decomposition

Distributed Topologies & Service Decomposition

Distributed architecture is a tool for independence, scale, isolation, and organizational fit. Fowler and Lewis emphasize independently deployable services organized around business capability; cloud architecture guidance adds the runtime cost: latency, partial failure, contract drift, debugging complexity, deployment coordination, and data consistency. Senior architects therefore ask which parts of the system need independent change, scaling, failure handling, or ownership badly enough to justify distribution.

Service decomposition begins with boundaries, not endpoints. A service should own a business capability, the data needed to enforce its invariants, and the operational responsibility for its runtime behavior. If a service cannot be deployed, observed, secured, and evolved independently, it may be a distributed module rather than a true service.

Code
left to right direction
rectangle "Decomposition Forces" as Forces
rectangle "Business Capability" as Capability
rectangle "Data Ownership" as Data
rectangle "Team Ownership" as Team
rectangle "Operational Independence" as Ops
rectangle "Service Candidate" as Service

Forces --> Capability
Forces --> Data
Forces --> Team
Forces --> Ops
Capability --> Service
Data --> Service
Team --> Service
Ops --> Service
Decomposition ForcesBusiness CapabilityData OwnershipTeam OwnershipOperational IndependenceService Candidate

Topology Choices

Distributed systems have topology. A request-driven topology routes synchronous calls through services. An event-driven topology publishes facts to a broker or log. A workflow topology uses orchestration or choreography to coordinate long-running business processes. A backend-for-frontend topology gives each user experience a tailored API. A mesh or gateway topology centralizes some cross-cutting routing and security behavior.

Each topology changes failure modes. A synchronous chain creates direct latency coupling: if inventory is slow, checkout may be slow. An event-driven topology reduces immediate coupling but creates eventual consistency and replay concerns. A workflow orchestrator clarifies process state but can become a central dependency. A gateway simplifies clients but can accumulate business logic if ownership is weak.

Code
left to right direction
actor "Client" as Client
rectangle "API Gateway" as Gateway
rectangle "Checkout Service" as Checkout
rectangle "Inventory Service" as Inventory
rectangle "Payment Service" as Payment
queue "Event Broker" as Broker
rectangle "Fulfillment Service" as Fulfillment

Client --> Gateway
Gateway --> Checkout : command
Checkout --> Inventory : reserve
Checkout --> Payment : authorize
Checkout --> Broker : OrderAccepted
Broker --> Fulfillment : start shipment
ClientAPI GatewayCheckout ServiceInventory ServicePayment ServiceEvent BrokerFulfillment ServicecommandreserveauthorizeOrderAcceptedstart shipment

Service Size and Responsibility

Service size is less important than service coherence. A small service with unclear ownership is worse than a larger service with a stable domain boundary. Services should be split when their reasons to change diverge, their scaling needs differ, their data ownership is independent, or separate teams need separate release cadence. They should not be split merely because classes feel large.

A common decomposition error is entity services: CustomerService, OrderService, ProductService, InvoiceService, each wrapping database tables and forcing business workflows to hop across network boundaries. Capability services are stronger: Checkout, Subscription Management, Claims Processing, Fraud Decisioning, Fulfillment Planning. Capability boundaries contain behavior, not just nouns.

Code
left to right direction
rectangle "Entity Service Trap" as Trap {
rectangle "Customer Service" as Customer
rectangle "Order Service" as Order
rectangle "Product Service" as Product
rectangle "Invoice Service" as Invoice
}
rectangle "Capability Boundary" as Capability {
rectangle "Checkout" as Checkout
rectangle "Subscription Management" as Subscription
rectangle "Fraud Decisioning" as Fraud
}
Customer -[#FF5555,dashed]-> Order : chatty workflow
Order -[#FF5555,dashed]-> Product : chatty workflow
Order -[#FF5555,dashed]-> Invoice : chatty workflow
Checkout --> Fraud : decision contract
Checkout --> Subscription : customer entitlement
Entity Service TrapCapability BoundaryCustomer ServiceOrder ServiceProduct ServiceInvoice ServiceCheckoutSubscription ManagementFraud Decisioningchatty workflowchatty workflowchatty workflowdecision contractcustomer entitlement

Coordination Models

Long-running workflows are unavoidable in distributed systems. Payment authorization, inventory reservation, shipment creation, invoicing, notification, and fraud review cannot always happen in one transaction. Coordination can be orchestrated by a workflow component or choreographed through events.

Orchestration makes process state explicit and easier to inspect. It can also centralize too much business knowledge. Choreography keeps services autonomous and extensible. It can also make the overall process difficult to understand unless events, tracing, and ownership are strong. The mature choice is not ideological. It depends on process criticality, need for visibility, number of participants, compensation complexity, and who owns the end-to-end outcome.

Code
left to right direction
rectangle "Order Workflow Orchestrator" as Orchestrator
rectangle "Payment" as Payment
rectangle "Inventory" as Inventory
rectangle "Fulfillment" as Fulfillment
rectangle "Notification" as Notification
database "Workflow State" as State

Orchestrator --> Payment : authorize
Orchestrator --> Inventory : reserve
Orchestrator --> Fulfillment : create shipment
Orchestrator --> Notification : send receipt
Orchestrator --> State : record step and compensation
Order Workflow OrchestratorPaymentInventoryFulfillmentNotificationWorkflow Stateauthorizereservecreate shipmentsend receiptrecord step and compensation

Operational Readiness

Every new service creates an operational surface: deployments, alerts, dashboards, logs, traces, secrets, certificates, dependency health, scaling policy, cost attribution, and incident ownership. If the platform does not make these cheap, service decomposition becomes organizational debt. A senior architecture proposal includes the operating model, not just the runtime boxes.

Service count should grow with platform maturity. Early decomposition can be useful for strong boundaries, but the organization must be able to operate the result. A team that cannot reliably answer “which service is failing, who owns it, and what changed?” should slow down distribution until observability and ownership are improved.

Code
rectangle "Service Readiness Checklist" as Checklist {
rectangle "Owner and on-call path" as Owner
rectangle "Deployment pipeline" as Deploy
rectangle "Dashboards and alerts" as Observe
rectangle "Contract versioning" as Contract
rectangle "Data ownership" as Data
rectangle "Runbook and rollback" as Runbook
}
Owner --> Deploy
Deploy --> Observe
Observe --> Contract
Contract --> Data
Data --> Runbook
Service Readiness ChecklistOwner and on-call pathDeployment pipelineDashboards and alertsContract versioningData ownershipRunbook and rollback

Practice

Choose a proposed service split and argue both sides. First, list the independence the split would create: deployment, scaling, ownership, security, or failure isolation. Then list the distribution costs: latency, consistency, observability, versioning, and operational burden. Decide whether to split now, create an internal module first, or delay until a specific trigger appears.

References & Further Reading

Section Detail

Integration, Contracts & Coupling

Integration, Contracts & Coupling

Integration is where architecture becomes social. A contract says what one party can depend on and what another party promises to preserve. Modern API, event-driven, and microservice guidance agree that the contract may be an HTTP API, event schema, file format, library interface, database view, command queue, or manual workflow. Good contracts reduce coordination. Poor contracts move hidden assumptions across boundaries until every change requires negotiation.

Coupling is not one thing. Systems can be coupled by time, data shape, availability, deployment, semantics, identity, operational process, and ownership. A synchronous API couples caller and provider in time and availability. A shared database couples consumers to storage structure. An event stream decouples time but couples consumers to event semantics and versioning. Shared libraries reduce duplication but couple release cadence.

Code
left to right direction
rectangle "Coupling Types" as Coupling
rectangle "Temporal" as Temporal
rectangle "Data Shape" as Data
rectangle "Semantic" as Semantic
rectangle "Availability" as Availability
rectangle "Deployment" as Deployment
rectangle "Operational" as Operational

Coupling --> Temporal
Coupling --> Data
Coupling --> Semantic
Coupling --> Availability
Coupling --> Deployment
Coupling --> Operational
Coupling TypesTemporalData ShapeSemanticAvailabilityDeploymentOperational

Synchronous Contracts

Synchronous APIs are appropriate when the caller needs an immediate answer to continue. They are often simpler for request-response interactions, validation, and user-facing flows. The contract should define behavior, not only fields: idempotency, error model, timeout expectation, authentication, authorization, rate limits, pagination, compatibility rules, and ownership of retries.

The danger is call-chain architecture. A user request enters one service and triggers five downstream calls, each with its own latency and failure behavior. Tail latency compounds and partial failures become user-visible. Synchronous contracts need budgets: latency budget, retry budget, dependency budget, and a plan for fallback or failure.

Code
left to right direction
actor "User" as User
rectangle "Web App" as Web
rectangle "Checkout API" as Checkout
rectangle "Pricing API" as Pricing
rectangle "Inventory API" as Inventory
rectangle "Payment API" as Payment

User --> Web
Web --> Checkout : submit order
Checkout --> Pricing : price basket
Checkout --> Inventory : reserve
Checkout --> Payment : authorize
rectangle "Latency Budget\n400 ms total" as Budget
Budget .. Checkout
UserWeb AppCheckout APIPricing APIInventory APIPayment APILatency Budget400 ms totalsubmit orderprice basketreserveauthorize

Event Contracts

Events are contracts around facts. They should be named in business language, versioned carefully, and documented with delivery expectations. Consumers need to know whether events are ordered, duplicated, delayed, replayable, retained, and backward compatible. Producers need to know which fields are contractual and which are incidental.

The strongest event contracts are additive and tolerant. Add fields without breaking consumers. Avoid changing meaning under the same name. Prefer new event versions or new event types when semantics change. Consumers should ignore unknown fields and use idempotency keys because event delivery commonly offers at-least-once semantics.

Code
left to right direction
rectangle "Producer" as Producer
queue "Event Stream\nOrderPaid v2" as Stream
rectangle "Consumer A\nFulfillment" as A
rectangle "Consumer B\nAnalytics" as B
rectangle "Consumer C\nMessaging" as C
rectangle "Schema Registry" as Registry

Producer --> Registry : validates schema
Producer --> Stream : publishes fact
Stream --> A : at least once
Stream --> B : replay projection
Stream --> C : customer receipt
Registry .. A : compatibility rules
Registry .. B : compatibility rules
Registry .. C : compatibility rules
ProducerEvent StreamOrderPaid v2Consumer AFulfillmentConsumer BAnalyticsConsumer CMessagingSchema Registryvalidates schemapublishes factat least oncereplay projectioncustomer receiptcompatibility rulescompatibility rulescompatibility rules

Files, Batches, and Data Products

Senior architecture should not pretend everything is an API. Many enterprise and analytical integrations are files, batches, extracts, or data products. These interfaces can be excellent when latency tolerance is high, auditability matters, or external partners cannot support interactive APIs. They can be terrible when they hide errors for days or create ambiguous ownership.

A batch contract should define schedule, schema, completeness guarantees, correction process, replay process, retention, encryption, and ownership of rejected records. A data product should define meaning, freshness, lineage, access policy, and consumer support. Treating files as low-status integration creates fragile architecture. Treating them as first-class contracts creates predictable systems.

Code
left to right direction
database "Operational Store" as Store
rectangle "Export Job" as Export
folder "Encrypted File Drop" as Drop
rectangle "Partner Import" as Partner
rectangle "Reconciliation Report" as Recon
rectangle "Exception Queue" as Exceptions

Store --> Export : nightly extract
Export --> Drop : signed file
Drop --> Partner : import
Partner --> Recon : accepted and rejected counts
Recon --> Exceptions : investigation
Operational StoreExport JobEncrypted File DropPartner ImportReconciliation ReportException Queuenightly extractsigned fileimportaccepted and rejected countsinvestigation

Compatibility and Versioning

Compatibility is an architectural quality. Backward compatibility means old consumers continue to work with new providers. Forward compatibility means new consumers can tolerate older providers or unknown fields. Contract testing helps, but it cannot replace ownership. Someone must decide what compatibility means, how long versions live, and how deprecation is communicated.

The most dangerous versioning strategy is no strategy. Teams add optional fields until optional no longer means optional, reuse field names with new semantics, or create endpoints that remain forever because no one tracks consumers. Mature architecture includes a contract lifecycle: propose, validate, publish, observe adoption, deprecate, and remove.

Code
left to right direction
rectangle "Contract Lifecycle" as Lifecycle {
rectangle "Design" as Design
rectangle "Consumer Review" as Review
rectangle "Compatibility Tests" as Tests
rectangle "Publish" as Publish
rectangle "Observe Usage" as Observe
rectangle "Deprecate" as Deprecate
rectangle "Remove" as Remove
}
Design --> Review
Review --> Tests
Tests --> Publish
Publish --> Observe
Observe --> Deprecate
Deprecate --> Remove
Contract LifecycleDesignConsumer ReviewCompatibility TestsPublishObserve UsageDeprecateRemove

Anti-Corruption Layers

An anti-corruption layer protects one model from another. It translates external concepts into local concepts and prevents legacy or vendor semantics from leaking into the core. The layer is not just a mapper. It is a boundary of meaning. If a vendor calls every customer an “account” but your domain distinguishes billing account, user, and legal party, the anti-corruption layer preserves that distinction.

This pattern is especially important during migrations. A new system may need to coexist with an old system for months. Without translation, the new model becomes polluted by legacy constraints. With a clear anti-corruption layer, migration work is visible, testable, and eventually removable.

Practice

Pick one integration and describe its contract in ten lines: owner, purpose, protocol, data shape, error behavior, compatibility rule, latency expectation, retry rule, observability signal, and deprecation path. Then identify two forms of coupling it creates and one architectural tactic that reduces each.

References & Further Reading

Section Detail

Data Architecture & Consistency

Data Architecture & Consistency

Data architecture is not just choosing a database. It is deciding who owns facts, where invariants are enforced, how data moves, what consistency users can rely on, how history is preserved, and how operational and analytical needs coexist. Microservice data patterns, event-sourcing literature, and cloud data guidance all converge on this point: many architecture failures are data failures wearing service costumes.

The first principle is ownership. A service or module owns the data whose validity it enforces. Other parts of the system may hold copies, projections, caches, or derived views, but they should not mutate the source of truth. Ownership is the foundation for schema evolution, privacy control, auditability, and operational recovery.

Code
left to right direction
rectangle "Source of Truth" as Source
rectangle "Published Event" as Event
database "Read Model" as Read
database "Cache" as Cache
database "Analytical Store" as Analytics
rectangle "Consumers" as Consumers

Source --> Event : publishes fact
Event --> Read : projection
Event --> Analytics : history
Source --> Cache : derived acceleration
Consumers --> Read : query
Consumers --> Cache : fast lookup
Source of TruthPublished EventRead ModelCacheAnalytical StoreConsumerspublishes factprojectionhistoryderived accelerationqueryfast lookup

Invariants and Transactions

An invariant is a rule that must remain true. “An order cannot be paid twice” is an invariant. “Inventory cannot drop below zero for a reserved item” may be an invariant. “A dashboard should update quickly” is not usually an invariant; it is a freshness goal. Distinguishing invariants from preferences is essential because invariants often determine transaction boundaries.

Strong consistency is valuable when violating a rule creates unacceptable harm: duplicate payments, unauthorized access, invalid ledger entries, or medical dosage errors. Eventual consistency is valuable when the system can tolerate delay and repair: search indexing, recommendations, notifications, analytics, and many fulfillment workflows. Senior architects do not worship either model. They attach consistency to business risk.

Code
left to right direction
rectangle "Business Rule" as Rule
rectangle "Decision\nMust be true immediately?" as Immediate
rectangle "Local Transaction Boundary" as Local
rectangle "Workflow with Compensation" as Workflow
rectangle "Projection or Cache" as Projection

Rule --> Immediate
Immediate --> Local : yes
Immediate --> Workflow : not always, but harm is manageable
Immediate --> Projection : no, freshness tolerance exists
Business RuleDecisionMust be true immediately?Local Transaction BoundaryWorkflow with CompensationProjection or Cacheyesnot always, but harm ismanageableno, freshness tolerance exists

CQRS and Read Models

Command Query Responsibility Segregation separates write behavior from read models. The write side enforces invariants and records facts. The read side optimizes queries for users, reporting, or integration. CQRS is useful when read and write needs differ significantly, when complex queries should not burden transactional models, or when multiple views are derived from the same facts.

CQRS is not a default. Fowler’s warning is practical: separating command and query models adds complexity and should be motivated by real asymmetry. It is usually justified when the read model has different shape, scale, latency, or ownership than the write model. A simple CRUD system does not become better by adding two models without a reason.

Code
left to right direction
actor "User" as User
rectangle "Command API" as Command
rectangle "Domain Model" as Domain
database "Write Store" as WriteStore
queue "Domain Events" as Events
rectangle "Projection Worker" as Worker
database "Read Model" as ReadModel
rectangle "Query API" as Query

User --> Command : change intent
Command --> Domain : enforce invariant
Domain --> WriteStore : commit
Domain --> Events : publish facts
Events --> Worker : project
Worker --> ReadModel : update view
User --> Query : read optimized view
Query --> ReadModel
UserCommand APIDomain ModelWrite StoreDomain EventsProjection WorkerRead ModelQuery APIchange intentenforce invariantcommitpublish factsprojectupdate viewread optimized view

Event Sourcing

Event sourcing stores state as a sequence of events rather than only the latest state. It is powerful when auditability, temporal queries, replay, and complex state transitions matter. It can be overkill when the domain is simple or when teams are not ready for event design, snapshots, migrations, and projection operations.

The hardest part of event sourcing is not the storage mechanism. It is choosing events that represent stable business facts. If events are too technical, history becomes brittle. If events are too vague, reconstruction becomes ambiguous. Event streams become part of the long-term contract of the system, so event design requires care.

Code
left to right direction
rectangle "Command\nApprove Claim" as Command
rectangle "Aggregate" as Aggregate
database "Event Stream" as Stream
rectangle "Projector" as Projector
database "Current View" as View
rectangle "Audit Timeline" as Audit

Command --> Aggregate : load history and decide
Aggregate --> Stream : append ClaimApproved
Stream --> Projector : replay
Projector --> View : current state
Stream --> Audit : immutable history
CommandApprove ClaimAggregateEvent StreamProjectorCurrent ViewAudit Timelineload history and decideappend ClaimApprovedreplaycurrent stateimmutable history

Analytical Separation

Operational systems and analytical systems have different quality attributes. Operational systems optimize correctness, low-latency transactions, and controlled change. Analytical systems optimize flexible query, historical breadth, aggregation, and exploration. Mixing them carelessly creates performance and ownership problems.

Analytical separation can be achieved through event streams, change data capture, exports, or data products. The key is not the technology but the contract: what data means, how fresh it is, who owns corrections, how privacy rules apply, and how lineage is tracked. Analytics that bypasses domain ownership can become a shadow architecture with its own truths.

Code
left to right direction
database "Operational Databases" as OLTP
rectangle "CDC or Events" as CDC
database "Data Lakehouse" as Lake
rectangle "Semantic Model" as Semantic
rectangle "BI and ML Consumers" as Consumers
rectangle "Data Governance" as Governance

OLTP --> CDC
CDC --> Lake
Lake --> Semantic
Semantic --> Consumers
Governance .. Lake : privacy, lineage, access
Governance .. Semantic : metric definitions
Operational DatabasesCDC or EventsData LakehouseSemantic ModelBI and ML ConsumersData Governanceprivacy, lineage, accessmetric definitions

Privacy and Retention

Data architecture must include retention, deletion, masking, encryption, access control, and lineage. Privacy is difficult to add later because data copies multiply. A single event may feed read models, caches, logs, data lakes, search indexes, and partner exports. If deletion or correction is required, the architecture must know where the data went.

Senior data architecture therefore tracks propagation paths. It also distinguishes immutable business history from removable personal data. Sometimes the answer is tokenization, cryptographic erasure, field-level encryption, or separating personal attributes from transactional events. The architectural goal is to preserve legitimate history while respecting privacy obligations.

Practice

Choose one business process and list its invariants. Decide which invariants require a local transaction and which can be managed by workflow, compensation, or projection. Then draw the source of truth, read models, analytical paths, and privacy-sensitive fields. Mark every copy of personal data.

References & Further Reading

Cross-Cutting Qualities

Section Detail

Runtime Architecture & Resilience

Runtime Architecture & Resilience

Runtime architecture is the shape the system takes when it is under pressure. The Google SRE books and cloud reliability frameworks both stress that reliability is observed under real conditions, not inferred from static structure. Diagrams drawn at rest often hide the important questions: what happens when a dependency is slow, a queue grows, a region loses capacity, a cache is cold, a deployment is bad, or a downstream system rejects traffic?

Distributed systems fail partially. One service can be healthy while its database is saturated. One region can accept traffic while another loses a provider. One dependency can respond slowly enough to exhaust caller threads without technically being down. Senior architecture treats partial failure as normal operating reality.

Code
left to right direction
actor "User" as User
rectangle "Edge" as Edge
rectangle "Application" as App
rectangle "Dependency A\nslow" as A
rectangle "Dependency B\nhealthy" as B
database "Database\nsaturated" as DB
queue "Queue\nbacklog" as Queue

User --> Edge
Edge --> App
App --> A : timeout risk
App --> B : normal
App --> DB : pool exhaustion risk
App --> Queue : delay risk
UserEdgeApplicationDependency AslowDependency BhealthyDatabasesaturatedQueuebacklogtimeout risknormalpool exhaustion riskdelay risk

Failure Modes

A failure mode is a specific way the system can stop meeting expectations. “Database down” is one. “Database slow enough to exhaust connection pools” is better. “Payment provider returns intermittent 500s while checkout retries without jitter and creates duplicate authorization attempts” is the level of specificity that leads to real design.

Failure-mode design asks five questions: how will we detect it, how will we contain it, how will the user experience degrade, how will we recover, and how will we know recovery is complete? These questions should be answered before production incidents write the architecture for you.

Code
rectangle "Failure Mode Design" as FMD {
rectangle "Detect\nmetric, log, trace, synthetic check" as Detect
rectangle "Contain\ntimeout, bulkhead, circuit breaker" as Contain
rectangle "Degrade\nfallback, queue, read-only mode" as Degrade
rectangle "Recover\nretry, replay, failover, rollback" as Recover
rectangle "Validate\nSLO restored, backlog drained, data reconciled" as Validate
}
Detect --> Contain
Contain --> Degrade
Degrade --> Recover
Recover --> Validate
Failure Mode DesignDetectmetric, log, trace, syntheticcheckContaintimeout, bulkhead, circuitbreakerDegradefallback, queue, read-only modeRecoverretry, replay, failover, rollbackValidateSLO restored, backlog drained,data reconciled

Timeouts, Retries, and Idempotency

Timeouts prevent callers from waiting forever. Retries handle transient failures. Backoff and jitter prevent synchronized retry storms. Idempotency ensures that repeating a command does not duplicate side effects. These tactics belong together. A retry policy without idempotency can charge twice, send duplicate emails, create duplicate orders, or corrupt downstream workflows.

Every outbound call should have a timeout that is shorter than the caller’s remaining latency budget. Every retry should have a reason, limit, and backoff. Every side-effecting operation should have an idempotency key or deduplication strategy. These are architectural policies because they shape failure propagation across the system.

Code
left to right direction
rectangle "Caller" as Caller
rectangle "Timeout Budget" as Budget
rectangle "Retry Policy\nlimited, backoff, jitter" as Retry
rectangle "Idempotency Key" as Key
rectangle "Provider" as Provider
database "Dedup Store" as Dedup

Caller --> Budget : checks remaining time
Budget --> Retry : permits retry?
Retry --> Key : repeats safely
Key --> Provider : command
Provider --> Dedup : reject duplicate side effect
CallerTimeout BudgetRetry Policylimited, backoff, jitterIdempotency KeyProviderDedup Storechecks remaining timepermits retry?repeats safelycommandreject duplicate side effect

Bulkheads and Backpressure

Bulkheads isolate failure by separating resource pools. A reporting workload should not exhaust the same database connections required for checkout. A slow partner integration should not consume every worker thread needed for core orders. Bulkheads can be thread pools, connection pools, queues, rate limits, service instances, database replicas, or even team ownership boundaries.

Backpressure tells upstream systems to slow down. Without it, queues grow, latency rises, autoscaling may add more pressure, and eventually the system collapses. Load shedding is a form of honest backpressure: reject low-priority work quickly so critical work can continue. A system that refuses some work can be more reliable than one that accepts everything and fails all of it later.

Code
left to right direction
actor "Users" as Users
rectangle "Ingress" as Ingress
rectangle "Priority Router" as Router
queue "Checkout Workers\nreserved pool" as Checkout
queue "Analytics Workers\nbest effort" as Analytics
database "Primary DB" as Primary
database "Replica" as Replica

Users --> Ingress
Ingress --> Router
Router --> Checkout : critical
Router --> Analytics : shed if overloaded
Checkout --> Primary
Analytics --> Replica
UsersIngressPriority RouterCheckout Workersreserved poolAnalytics Workersbest effortPrimary DBReplicacriticalshed if overloaded

Graceful Degradation

Graceful degradation preserves core value while reducing non-critical behavior. A commerce site may turn off recommendations, delay emails, switch to cached catalog data, or place suspicious orders into review while preserving checkout. A collaboration tool may disable search indexing while preserving document editing. Degradation must be product-designed; engineering cannot invent it during an incident without risking user trust.

Degradation levels should be explicit. Level one may reduce optional features. Level two may enable read-only mode. Level three may restrict traffic to existing customers. Each level needs trigger conditions, user messaging, owner approval, and recovery validation. This turns resilience from heroics into designed behavior.

Code
rectangle "Degradation Policy" as Policy {
rectangle "Level 0\nnormal" as L0
rectangle "Level 1\ndisable non-critical personalization" as L1
rectangle "Level 2\nqueue low-priority writes" as L2
rectangle "Level 3\nread-only or restricted access" as L3
}
L0 --> L1 : dependency latency high
L1 --> L2 : backlog exceeds threshold
L2 --> L3 : error budget burn critical
L3 --> L0 : validation complete
Degradation PolicyLevel 0normalLevel 1disable non-criticalpersonalizationLevel 2queue low-priority writesLevel 3read-only or restricted accessdependency latency highbacklog exceeds thresholderror budget burn criticalvalidation complete

Capacity and Recovery

Capacity is not only average throughput. Tail latency, burst tolerance, queue drain time, cold-start behavior, cache refill, database connection limits, and downstream quotas all matter. A system that handles normal traffic but cannot drain a backlog after a one-hour outage is not resilient.

Recovery is part of architecture. Backups are not enough; restoration must be rehearsed. Multi-region failover is not enough; traffic routing, data replication lag, secrets, certificates, and operational authority must be tested. A retry mechanism is not enough; idempotency and reconciliation must prove that recovery did not duplicate or lose work.

Practice

Pick one critical user journey and write a failure-mode table with five rows: slow dependency, unavailable dependency, database saturation, queue backlog, and bad deployment. For each, define detection, containment, degraded user experience, recovery action, and validation signal.

References & Further Reading

Section Detail

Security, Privacy & Trust Boundaries

Security, Privacy & Trust Boundaries

Security architecture begins with trust boundaries. NIST’s zero-trust guidance treats implicit network trust as unsafe and emphasizes explicit, contextual access decisions. In software architecture, a trust boundary is any place where assumptions change: public internet to edge, user device to backend, service to database, internal network to vendor, tenant A to tenant B, employee to production, or application code to secrets store.

Senior architecture does not bolt security on after the structure is chosen. It asks where identity is established, where authorization decisions are made, where data changes sensitivity, where secrets live, where audit evidence is produced, where blast radius is contained, and where privacy obligations follow data. The design should make safe behavior the default path.

Code
left to right direction
actor "User Device\nuntrusted" as Device
rectangle "Edge\nTLS, WAF, rate limits" as Edge
rectangle "Application Zone\nleast privilege services" as App
database "Data Zone\ncontrolled access" as Data
rectangle "Vendor API\nexternal trust" as Vendor
rectangle "Security Monitoring" as Monitor

Device --> Edge : public boundary
Edge --> App : authenticated request
App --> Data : authorized query
App --> Vendor : outbound policy
Edge --> Monitor : access events
App --> Monitor : audit events
Data --> Monitor : sensitive access
User DeviceuntrustedEdgeTLS, WAF, rate limitsApplication Zoneleast privilege servicesData Zonecontrolled accessVendor APIexternal trustSecurity Monitoringpublic boundaryauthenticated requestauthorized queryoutbound policyaccess eventsaudit eventssensitive access

Threat Modeling as Design

Threat modeling is architecture analysis under adversarial conditions. It asks what assets matter, who might attack or misuse them, which boundaries they cross, what can go wrong, and what controls reduce risk. The output should influence design decisions, not merely create a compliance artifact.

Useful threat models are concrete. “An attacker steals customer data” is too broad. “A compromised support account exports all tenant records because the admin API authorizes by role only and has no tenant scoping, export rate limit, or audit alert” is designable. It points to controls: scoped authorization, just-in-time elevation, export limits, approval workflow, anomaly detection, and audit review.

Code
rectangle "Threat Model Loop" as Loop {
rectangle "Assets\ndata, money, availability, reputation" as Assets
rectangle "Actors\nexternal attacker, insider, compromised service" as Actors
rectangle "Entry Points\nAPIs, jobs, admin tools, vendors" as Entry
rectangle "Threats\nspoof, tamper, disclose, deny, escalate" as Threats
rectangle "Controls\nprevent, detect, respond, recover" as Controls
}
Assets --> Actors
Actors --> Entry
Entry --> Threats
Threats --> Controls
Controls --> Assets : residual risk review
Threat Model LoopAssetsdata, money, availability,reputationActorsexternal attacker, insider,compromised serviceEntry PointsAPIs, jobs, admin tools, vendorsThreatsspoof, tamper, disclose, deny,escalateControlsprevent, detect, respond,recoverresidual risk review

Identity and Authorization

Authentication answers who the caller is. Authorization answers what the caller may do in this context. Architecture failures often come from mixing these questions. A valid token does not mean the caller may access this tenant, approve this refund, read this medical record, or call this internal endpoint. Authorization needs domain context.

Centralized identity can reduce duplication, but authorization often belongs near the domain that understands the resource. A policy engine may help if policies are complex and shared, but the system still needs clear ownership of policy meaning. The contract should define subject, action, resource, environment, and decision evidence. Audit logs should capture why sensitive access was allowed, not merely that a request succeeded.

Code
left to right direction
actor "Caller" as Caller
rectangle "Identity Provider" as IdP
rectangle "API Gateway" as Gateway
rectangle "Domain Service" as Service
rectangle "Policy Decision Point" as PDP
database "Resource State" as Resource
database "Audit Log" as Audit

Caller --> IdP : authenticate
Caller --> Gateway : token
Gateway --> Service : verified identity
Service --> PDP : subject, action, resource
PDP --> Resource : context
PDP --> Service : allow or deny
Service --> Audit : decision evidence
CallerIdentity ProviderAPI GatewayDomain ServicePolicy Decision PointResource StateAudit Logauthenticatetokenverified identitysubject, action, resourceallow or denycontextdecision evidence

Secrets and Supply Chain

Secrets architecture defines how credentials, keys, tokens, certificates, and signing material are created, stored, rotated, accessed, and revoked. Secrets in source code, logs, build artifacts, or shared configuration are architectural risks because they create large blast radius. A mature design gives workloads short-lived credentials, narrow permissions, rotation paths, and observability of secret use.

Supply-chain security belongs in architecture because modern systems are assembled from dependencies, containers, CI/CD pipelines, infrastructure modules, and third-party services. The runtime boundary begins in the build pipeline. Signing, provenance, dependency scanning, artifact promotion, and environment separation are not bureaucratic extras; they protect the integrity of what reaches production.

Code
left to right direction
rectangle "Source" as Source
rectangle "CI Pipeline" as CI
rectangle "Dependency Scan" as Scan
rectangle "Build Artifact" as Artifact
rectangle "Signature and Provenance" as Sign
rectangle "Artifact Registry" as Registry
rectangle "Production Deploy" as Deploy
rectangle "Secrets Manager" as Secrets

Source --> CI
CI --> Scan
Scan --> Artifact
Artifact --> Sign
Sign --> Registry
Registry --> Deploy
Deploy --> Secrets : short-lived credentials
SourceCI PipelineDependency ScanBuild ArtifactSignature and ProvenanceArtifact RegistryProduction DeploySecrets Managershort-lived credentials

Privacy Architecture

Privacy is not only access control. It includes data minimization, purpose limitation, consent, retention, deletion, correction, encryption, masking, lineage, and cross-border movement. Architecture should know where personal data enters, where it is copied, which fields are sensitive, and how obligations are enforced across logs, caches, analytics, backups, and vendors.

One strong tactic is data classification at boundaries. Public, internal, confidential, personal, and regulated data should not flow through the system with equal treatment. Another is separation: keep personal identifiers separate from event history where possible, use tokenization or pseudonymization, and design deletion as a known workflow instead of a heroic database search.

Code
left to right direction
rectangle "User Profile Service" as Profile
database "PII Store" as PII
queue "Domain Events\nminimal personal data" as Events
database "Analytics Store\npseudonymous ids" as Analytics
rectangle "Deletion Workflow" as Delete
rectangle "Vendor Export" as Vendor
rectangle "Privacy Controls" as Controls

Profile --> PII : owns personal data
Profile --> Events : publishes minimal facts
Events --> Analytics : pseudonymous projection
Profile --> Vendor : purpose-limited export
Controls .. PII : encryption, retention, access
Delete --> PII : erase or anonymize
Delete --> Analytics : remove linkage
Delete --> Vendor : deletion request
User Profile ServicePII StoreDomain Eventsminimal personal dataAnalytics Storepseudonymous idsDeletion WorkflowVendor ExportPrivacy Controlsowns personal datapublishes minimal factspseudonymous projectionpurpose-limited exportencryption, retention, accesserase or anonymizeremove linkagedeletion request

Blast Radius

Security design should assume some control will fail. Blast radius asks what an attacker, bug, or misconfigured service can do after one credential, service, tenant, region, or account is compromised. Narrow blast radius comes from least privilege, network segmentation, tenant isolation, scoped credentials, rate limits, approval workflows, immutable logs, and fast revocation.

The ideal is not infinite prevention. It is layered control: prevent common attacks, detect unusual behavior, contain damage, recover safely, and learn. A control that cannot be operated during an incident is incomplete. A highly secure design that no team can maintain will decay into exceptions and bypasses.

Practice

Choose one sensitive workflow such as refund approval, medical record access, payroll export, or production database query. Draw the trust boundaries, identify the assets, name three threats, and propose one preventive, one detective, and one recovery control. Then state the residual risk in business language.

References & Further Reading

Section Detail

Platform, Deployment & Operations

Platform, Deployment & Operations

Architecture does not end at code boundaries. Platform engineering and cloud well-architected guidance treat delivery, runtime, observability, security, and recovery as part of the system’s design environment. A beautifully decomposed system that is painful to deploy is not architecturally successful. A platform that makes good defaults easy can improve every product team without requiring every team to become infrastructure specialists.

Platform architecture should reduce cognitive load while preserving appropriate autonomy. The goal is not to centralize every decision. The goal is to provide paved roads: standard deployment pipelines, service templates, observability, secret handling, traffic management, policy checks, and runtime environments that product teams can use without rediscovering every operational practice.

Code
left to right direction
rectangle "Product Teams" as Teams
rectangle "Paved Road Platform" as Platform {
rectangle "Service Template" as Template
rectangle "CI/CD" as CICD
rectangle "Runtime" as Runtime
rectangle "Observability" as Observability
rectangle "Secrets and Policy" as Policy
}
rectangle "Cloud Infrastructure" as Cloud
rectangle "Production Systems" as Prod

Teams --> Template : start service
Teams --> CICD : deliver changes
CICD --> Runtime : deploy
Runtime --> Cloud : provisioned capacity
Runtime --> Prod
Observability --> Teams : feedback
Policy --> Runtime : guardrails
Paved Road PlatformService TemplateCI/CDRuntimeObservabilitySecrets and PolicyProduct TeamsCloud InfrastructureProduction Systemsstart servicedeliver changesdeployprovisioned capacityfeedbackguardrails

Deployment Topology

Deployment topology describes where components run and how traffic reaches them. It includes regions, zones, clusters, networks, gateways, service discovery, databases, queues, caches, and external dependencies. Deployment topology affects latency, availability, compliance, cost, and incident response.

A single-region deployment may be perfectly appropriate for an early product if recovery time expectations are modest. Multi-zone redundancy can handle many infrastructure failures without the complexity of active-active multi-region design. Multi-region systems are powerful, but they introduce data replication, consistency, failover, routing, cost, and operational authority challenges. Senior design chooses topology based on explicit recovery and availability goals.

Code
left to right direction
cloud "Internet" as Internet
rectangle "Global DNS / Traffic Manager" as DNS
node "Region A" as A {
node "Zone A1" as A1 {
  rectangle "App Instances" as AppA1
}
node "Zone A2" as A2 {
  rectangle "App Instances" as AppA2
}
database "Primary Database" as Primary
}
node "Region B\nwarm standby" as B {
rectangle "Standby App" as AppB
database "Replica Database" as Replica
}

Internet --> DNS
DNS --> AppA1
DNS --> AppA2
Primary --> Replica : replication
DNS .. AppB : failover route
Region AZone A1Zone A2Region Bwarm standbyPrimary DatabaseApp InstancesApp InstancesStandby AppReplica DatabaseInternetGlobal DNS / Traffic Managerreplicationfailover route

Release Strategies

Deployment and release are different. Deployment puts code into an environment. Release exposes behavior to users. Separating them with feature flags, progressive delivery, canaries, blue-green deployments, and traffic shaping reduces risk. Architecture should make rollback and roll-forward practical. It should also include data migration strategy, because database changes often determine whether rollback is safe.

Progressive delivery is most valuable when telemetry can detect harm quickly. A canary without good metrics is theater. A feature flag without ownership becomes permanent complexity. A blue-green environment without data compatibility may still fail. Release architecture connects rollout mechanism, observability, data evolution, and decision authority.

Code
left to right direction
rectangle "Commit" as Commit
rectangle "Build and Test" as Build
rectangle "Deploy Dark" as Dark
rectangle "Canary 5%" as Canary
rectangle "Progressive Rollout" as Rollout
rectangle "Full Release" as Full
rectangle "Rollback or Disable Flag" as Rollback
rectangle "Telemetry Gate" as Telemetry

Commit --> Build
Build --> Dark
Dark --> Canary
Canary --> Telemetry
Telemetry --> Rollout : healthy
Telemetry --> Rollback : unhealthy
Rollout --> Full
CommitBuild and TestDeploy DarkCanary 5%Progressive RolloutFull ReleaseRollback or Disable FlagTelemetry Gatehealthyunhealthy

Configuration and Environment Boundaries

Configuration is architectural because it changes behavior without code. Environment variables, feature flags, tenant settings, policy rules, rate limits, connection strings, and secrets all shape runtime behavior. Misconfiguration can be as damaging as a code defect. Configuration needs ownership, validation, audit, rollout, and rollback.

Environment parity matters, but perfect parity is often impossible. Instead, design for controlled differences. Development may use lightweight dependencies. Staging may use production-like topology with synthetic data. Production may have stricter policy and scale. The architecture should document which differences are acceptable and which invalidate testing.

Code
rectangle "Configuration Lifecycle" as Config {
rectangle "Define owner and schema" as Define
rectangle "Validate before deploy" as Validate
rectangle "Apply gradually" as Apply
rectangle "Audit change" as Audit
rectangle "Rollback known good value" as Rollback
}
Define --> Validate
Validate --> Apply
Apply --> Audit
Audit --> Rollback
Rollback --> Validate
Configuration LifecycleDefine owner and schemaValidate before deployApply graduallyAudit changeRollback known good value

Operability as a Feature

Operability means the system can be understood, controlled, repaired, and improved in production. It includes health checks, dashboards, logs, traces, metrics, runbooks, admin tools, backfills, replay, data repair, circuit breaker control, feature flag control, and incident communication. These are not afterthoughts. They are features for the people who keep the system alive.

Architectural decisions should include operational consequences. If the system uses async workflows, operators need queue visibility and replay controls. If the system uses caches, operators need invalidation and freshness signals. If the system uses multi-region failover, operators need rehearsed procedures and clear authority. A runtime without control surfaces invites manual database edits and risky emergency scripts.

Code
left to right direction
rectangle "Production System" as System
rectangle "Control Plane" as Control {
rectangle "Feature Flags" as Flags
rectangle "Circuit Breakers" as Breakers
rectangle "Replay and Backfill" as Replay
rectangle "Admin Workflows" as Admin
}
rectangle "Observation Plane" as Observe {
rectangle "Metrics" as Metrics
rectangle "Logs" as Logs
rectangle "Traces" as Traces
rectangle "SLOs" as SLO
}

Control --> System : controlled change
System --> Observe : emits signals
Observe --> Control : informed action
Control PlaneObservation PlaneFeature FlagsCircuit BreakersReplay and BackfillAdmin WorkflowsMetricsLogsTracesSLOsProduction Systemcontrolled changeemits signalsinformed action

Practice

Draw the deployment topology for a critical system. Add recovery time objective, recovery point objective, rollout strategy, configuration ownership, and operator control points. Then identify one manual production action that currently exists and design a safer operational interface for it.

References & Further Reading

Evolution

Section Detail

Observability & Architecture Fitness

Observability & Architecture Fitness

Architecture needs feedback. Without feedback, diagrams become wishes and decisions become folklore. OpenTelemetry’s model of traces, metrics, and logs supplies runtime evidence; evolutionary-architecture practice adds design evidence through automated fitness functions that verify dependency rules, contract compatibility, latency budgets, security policies, cost thresholds, and resilience expectations.

The goal is not to monitor everything. The goal is to know whether the system is meeting the qualities it was designed to protect. If availability is a top quality, the architecture needs SLOs and error-budget signals. If modifiability is a top quality, the architecture needs dependency checks, cycle detection, module ownership, and lead-time tracking. If cost efficiency matters, the architecture needs unit economics and capacity signals.

Code
left to right direction
rectangle "Architecture Intent" as Intent
rectangle "Runtime Telemetry" as Telemetry
rectangle "Fitness Functions" as Fitness
rectangle "Decision Review" as Review
rectangle "Architecture Evolution" as Evolution

Intent --> Telemetry : what to observe
Intent --> Fitness : what to verify
Telemetry --> Review : evidence
Fitness --> Review : evidence
Review --> Evolution : change decisions
Evolution --> Intent : updated intent
Architecture IntentRuntime TelemetryFitness FunctionsDecision ReviewArchitecture Evolutionwhat to observewhat to verifyevidenceevidencechange decisionsupdated intent

Observability for Architecture

Observability is the ability to understand system behavior from emitted signals. For architecture, the most useful signals often show relationships: service dependency maps, trace waterfalls, queue depth, saturation, error-budget burn, cache hit rates, database wait events, contract errors, deployment correlations, and tenant-level behavior.

Logs explain events. Metrics quantify trends. Traces show causality across boundaries. Events capture domain facts. Profiles reveal resource use. None is sufficient alone. A trace may show that checkout is slow because payment is slow, while metrics show the error-budget impact, logs show provider rejection details, and domain events reveal how many orders are stuck.

Code
left to right direction
rectangle "User Journey\nCheckout" as Journey
rectangle "Trace" as Trace
rectangle "Metrics" as Metrics
rectangle "Logs" as Logs
rectangle "Domain Events" as Events
rectangle "Architectural Insight" as Insight

Journey --> Trace : causal path
Journey --> Metrics : latency and errors
Journey --> Logs : detailed context
Journey --> Events : business progress
Trace --> Insight
Metrics --> Insight
Logs --> Insight
Events --> Insight
User JourneyCheckoutTraceMetricsLogsDomain EventsArchitectural Insightcausal pathlatency and errorsdetailed contextbusiness progress

SLOs and Error Budgets

Service-level objectives connect architecture to user experience. An SLO might say that 99.9 percent of checkout attempts complete successfully within two seconds over thirty days, excluding invalid payment details. The exact wording matters because it defines what users care about and what the team will optimize.

Error budgets create decision pressure. If a service is burning budget too quickly, reliability work becomes more important than feature release. If the service is comfortably within budget, the team may accept more change risk. This turns reliability from an abstract virtue into a management mechanism. The architecture should support measuring the SLO directly, not through proxies that hide user pain.

Code
left to right direction
actor "User" as User
rectangle "Critical Journey" as Journey
rectangle "SLI\nsuccess latency" as SLI
rectangle "SLO\n99.9 percent target" as SLO
rectangle "Error Budget" as Budget
rectangle "Release Decision" as Release
rectangle "Reliability Work" as Reliability

User --> Journey
Journey --> SLI
SLI --> SLO
SLO --> Budget
Budget --> Release : healthy
Budget --> Reliability : burning fast
UserCritical JourneySLIsuccess latencySLO99.9 percent targetError BudgetRelease DecisionReliability Workhealthyburning fast

Architecture Fitness Functions

A fitness function is an executable check that tells whether an architectural property still holds. It might fail the build if a domain module imports infrastructure, if an API change breaks a consumer contract, if a Terraform policy exposes a public database, if a service exceeds a latency budget in a performance test, or if a container image contains a critical vulnerability.

Fitness functions should be few, meaningful, and connected to decisions. Too many checks create noise. Too few checks let architecture decay. The best checks are those that prevent expensive drift: dependency direction, module boundaries, contract compatibility, security invariants, migration safety, and operational readiness.

Code
rectangle "Fitness Function Suite" as Suite {
rectangle "Dependency Rule Check" as Dep
rectangle "Contract Compatibility Test" as Contract
rectangle "Security Policy Check" as Security
rectangle "Performance Budget Test" as Perf
rectangle "Cost Threshold Alert" as Cost
}
rectangle "Pipeline" as Pipeline
rectangle "Production Telemetry" as Prod
rectangle "Architecture Review" as Review

Pipeline --> Dep
Pipeline --> Contract
Pipeline --> Security
Pipeline --> Perf
Prod --> Cost
Dep --> Review
Contract --> Review
Security --> Review
Perf --> Review
Cost --> Review
Fitness Function SuiteDependency Rule CheckContract Compatibility TestSecurity Policy CheckPerformance Budget TestCost Threshold AlertPipelineProduction TelemetryArchitecture Review

Socio-Technical Metrics

Architecture is socio-technical, so some fitness signals come from delivery and collaboration. Lead time, deployment frequency, change failure rate, time to restore, code ownership concentration, dependency wait time, review bottlenecks, and onboarding friction can expose architectural problems. If a simple feature requires five teams and three release windows, the architecture is communicating through delay.

These metrics should be interpreted carefully. They are signals, not weapons. DORA’s current guidance explicitly stresses application context and continuous improvement, so a high change failure rate should trigger diagnosis rather than blame. It may indicate brittle tests, unclear ownership, risky deployment, or excessive coupling. A long lead time may indicate compliance gates, unclear requirements, or architecture that forces cross-team coordination. Senior architects use the metrics to ask better questions, not to shame teams.

Code
left to right direction
rectangle "Architecture Health" as Health
rectangle "Runtime Signals" as Runtime
rectangle "Delivery Signals" as Delivery
rectangle "Team Signals" as Team
rectangle "User Signals" as User

Runtime --> Health : latency, errors, saturation
Delivery --> Health : lead time, deploy frequency
Team --> Health : ownership, cognitive load
User --> Health : task success, complaints
Health --> Runtime : improvement hypotheses
Architecture HealthRuntime SignalsDelivery SignalsTeam SignalsUser Signalslatency, errors, saturationimprovement hypotheseslead time, deploy frequencyownership, cognitive loadtask success, complaints

Feedback Cadence

Feedback has cadence. Some checks run on every commit. Some run nightly. Some are reviewed weekly. Some appear during quarterly architecture review. The cadence should match the risk. A public database exposure should fail immediately. A cost trend may need weekly review. A domain boundary concern may need review when change coordination rises.

Architecture review should be evidence-based. Instead of asking whether the system is “clean,” ask which decision assumptions are still true, which fitness functions are failing, which quality scenarios are at risk, and which options are closing. This makes architecture evolution a normal engineering practice rather than a special ceremony.

Practice

Choose three decisions from earlier modules and define one fitness function for each. At least one should be a pipeline check, one should be a production telemetry check, and one should be a delivery or organizational signal. State what action should happen when each check fails.

References & Further Reading

Section Detail

Evolutionary Architecture & Governance

Evolutionary Architecture & Governance

Architecture is never finished. Evolutionary-architecture literature treats change as a first-class design force: markets change, teams change, scale changes, regulations change, vendor capabilities change, and the codebase teaches you things the original design could not know. Adaptability is preserved through modularity, fitness functions, decision records, migration paths, and lightweight governance.

Governance has a bad reputation because it is often confused with approval theater. Good governance helps teams make better decisions faster. It provides shared principles, reference architectures, standards, review paths, and escalation mechanisms. It also makes exceptions explicit. The aim is coherence without freezing delivery.

Code
left to right direction
rectangle "Architecture Principles" as Principles
rectangle "Team Decisions" as Decisions
rectangle "Fitness Functions" as Fitness
rectangle "Review Forums" as Forums
rectangle "Exceptions" as Exceptions
rectangle "Learning Loop" as Learning

Principles --> Decisions : guide
Decisions --> Fitness : verified by
Fitness --> Forums : evidence
Forums --> Exceptions : approve with expiry
Exceptions --> Learning : reveal pressure
Learning --> Principles : refine
Architecture PrinciplesTeam DecisionsFitness FunctionsReview ForumsExceptionsLearning Loopguideverified byevidenceapprove with expiryreveal pressurerefine

Architecture Runway

Architecture runway is the enabling technical work that lets future product work land safely. It might include identity foundations before enterprise features, event infrastructure before integration growth, deployment automation before service decomposition, or data classification before regional expansion. Runway is not speculative gold plating. It is preparation tied to known near-future demand.

The discipline is timing. Too little runway causes teams to bolt features onto weak foundations. Too much runway creates unused platforms and abstraction debt. The best runway work has a named product driver, a short horizon, an adoption path, and a fitness signal. It should reduce future friction that is already visible.

Code
left to right direction
rectangle "Product Roadmap" as Roadmap
rectangle "Known Architectural Gap" as Gap
rectangle "Runway Work" as Runway
rectangle "Adoption by Teams" as Adoption
rectangle "Future Feature Flow" as Flow
rectangle "Fitness Signal" as Fitness

Roadmap --> Gap
Gap --> Runway
Runway --> Adoption
Adoption --> Flow
Flow --> Fitness
Fitness --> Roadmap : confirms or revises
Product RoadmapKnown Architectural GapRunway WorkAdoption by TeamsFuture Feature FlowFitness Signalconfirms or revises

Standards and Exceptions

Standards encode decisions that should not be relitigated by every team: logging fields, health checks, service templates, authentication integration, dependency scanning, API versioning, data classification, and deployment pipelines. Standards reduce cognitive load when they are small, justified, and supported by tooling.

Exceptions are healthy when they are explicit. A team may need a non-standard database, external provider, or deployment model. The exception should name the reason, owner, risk, compensating controls, and expiry or review date. This prevents standards from becoming prison bars and exceptions from becoming invisible fragmentation.

Code
rectangle "Standard Decision Path" as Standard {
rectangle "Use paved road" as Paved
rectangle "Automatic checks" as Checks
rectangle "Normal support" as Support
}
rectangle "Exception Path" as Exception {
rectangle "Explain driver" as Driver
rectangle "Assess risk" as Risk
rectangle "Define compensating controls" as Controls
rectangle "Set review date" as Review
}
Paved --> Checks
Checks --> Support
Driver --> Risk
Risk --> Controls
Controls --> Review
Standard Decision PathException PathUse paved roadAutomatic checksNormal supportExplain driverAssess riskDefine compensating controlsSet review date

Migration Architecture

Large architecture changes are migrations, not switches. Moving from monolith to services, replacing a database, changing identity provider, regionalizing data, or adopting an event backbone requires coexistence. The old and new systems must run together while traffic, data, and behavior move safely.

Common migration tactics include strangler fig, expand-contract database changes, dual writes with reconciliation, event backfill, shadow reads, traffic mirroring, compatibility adapters, and phased tenant migration. Each tactic has risks. Dual writes can diverge. Shadow reads can miss side effects. Strangler layers can become permanent. Migration architecture needs checkpoints and removal plans.

Code
left to right direction
actor "Client" as Client
rectangle "Strangler Facade" as Facade
rectangle "Legacy Capability" as Legacy
rectangle "New Capability" as New
database "Legacy Data" as LegacyDB
database "New Data" as NewDB
rectangle "Reconciliation" as Recon

Client --> Facade
Facade --> Legacy : old routes
Facade --> New : migrated routes
Legacy --> LegacyDB
New --> NewDB
LegacyDB --> Recon
NewDB --> Recon
Recon --> Facade : migration confidence
ClientStrangler FacadeLegacy CapabilityNew CapabilityLegacy DataNew DataReconciliationold routesmigrated routesmigration confidence

Technical Debt and Option Value

Technical debt is not all bad code. It is a design liability where a past choice makes future change more expensive. Some debt is rational: a startup may intentionally defer multi-region support to learn faster. Some debt is accidental: no one realized shared database access would block schema evolution. The architectural task is to price debt in terms of risk, delay, cost, and lost options.

Option value is the benefit of keeping a future path open. A modular boundary has option value because it may allow extraction later. A provider abstraction has option value if provider replacement is plausible. But options cost money. Keeping every option open creates complexity now. Senior architects buy options selectively, based on uncertainty and consequence.

Code
left to right direction
rectangle "Architectural Choice" as Choice
rectangle "Immediate Simplicity" as Simple
rectangle "Future Option" as Option
rectangle "Carrying Cost" as Cost
rectangle "Review Trigger" as Trigger

Choice --> Simple : may optimize
Choice --> Option : may preserve
Option --> Cost : costs now
Trigger --> Choice : revisit when uncertainty resolves
Architectural ChoiceImmediate SimplicityFuture OptionCarrying CostReview Triggermay optimizemay preservecosts nowrevisit when uncertainty resolves

Architecture Forums

An architecture forum should be a place for decision quality, not status reporting. Good forums review high-impact decisions, share learning, identify cross-team risks, approve exceptions, and retire outdated standards. They should be small enough to move and broad enough to represent important perspectives: product, engineering, operations, security, data, and sometimes support or compliance.

The forum should not own every decision. Teams should own local decisions within guardrails. The forum should focus on decisions that affect multiple teams, change shared standards, create long-term coupling, or expose significant risk. This keeps architecture governance lightweight and useful.

Practice

Design a governance model for a company with eight product teams and one platform team. Define which decisions teams can make locally, which require review, which standards are mandatory, how exceptions work, and how decisions are revisited. Then define three fitness functions that make the governance executable.

References & Further Reading

Section Detail

Architecture Review & Case Studies

Architecture Review & Case Studies

The purpose of architecture review is not to bless a diagram. SEI ATAM-style evaluation asks whether decisions satisfy quality-attribute scenarios, expose risks, and make tradeoffs explicit. A strong review helps teams move with confidence because it connects drivers, decisions, risks, and feedback. A weak review becomes a presentation ritual where hard questions arrive after implementation.

Senior reviews are scenario-driven. Instead of asking whether a design uses the right pattern, ask how it behaves under change, load, failure, misuse, growth, migration, and operation. The review should surface assumptions and decide which assumptions need evidence. The outcome is not always approval or rejection. Often it is a refined decision, a small experiment, a missing owner, or a staged migration plan.

Code
left to right direction
rectangle "Architecture Review" as Review
rectangle "Drivers" as Drivers
rectangle "Quality Scenarios" as Scenarios
rectangle "Decisions and Alternatives" as Decisions
rectangle "Risks and Mitigations" as Risks
rectangle "Fitness Signals" as Fitness
rectangle "Outcome" as Outcome

Drivers --> Review
Scenarios --> Review
Decisions --> Review
Risks --> Review
Fitness --> Review
Review --> Outcome
Architecture ReviewDriversQuality ScenariosDecisions and AlternativesRisks and MitigationsFitness SignalsOutcome

Review Inputs

A useful review package is concise. It should include context, goals, non-goals, key quality scenarios, domain boundaries, data ownership, integration contracts, deployment topology, security and privacy concerns, operational model, alternatives considered, risks, and open questions. The package should be small enough that reviewers can read it before the meeting.

The most important input is the decision frame. What decision is being made now? What options are still open? What is irreversible? What evidence exists? What evidence is missing? Without this frame, reviews drift into personal preference and pattern advocacy.

Code
rectangle "Review Packet" as Packet {
rectangle "Context and drivers" as Context
rectangle "Quality scenarios" as Quality
rectangle "Boundaries and data ownership" as Boundaries
rectangle "Runtime and deployment view" as Runtime
rectangle "Security and privacy view" as Security
rectangle "Alternatives and tradeoffs" as Tradeoffs
rectangle "Risks and open questions" as Risks
}
Context --> Quality
Quality --> Boundaries
Boundaries --> Runtime
Runtime --> Security
Security --> Tradeoffs
Tradeoffs --> Risks
Review PacketContext and driversQuality scenariosBoundaries and data ownershipRuntime and deployment viewSecurity and privacy viewAlternatives and tradeoffsRisks and open questions

Case Study: Marketplace Checkout

Consider a marketplace where buyers purchase from third-party sellers. The business drivers are conversion, payment correctness, seller independence, fraud control, and expansion to new regions. The quality scenarios include: checkout remains available during recommendation failure; duplicate payment is impossible; tax and compliance rules vary by region; fulfillment can lag but must be visible; fraud review may pause risky orders; support can explain order state.

A naive design might make checkout synchronously call pricing, tax, inventory, payment, fraud, fulfillment, notification, and analytics. It looks straightforward but creates a fragile latency chain. A better design might keep pricing, inventory reservation, and payment authorization in the critical path while publishing order events for fulfillment, notifications, analytics, and support projections. Fraud decisioning may be synchronous for high-risk baskets and asynchronous for low-risk monitoring, depending on business policy.

Code
left to right direction
actor "Buyer" as Buyer
rectangle "Checkout" as Checkout
rectangle "Pricing" as Pricing
rectangle "Inventory" as Inventory
rectangle "Payment" as Payment
rectangle "Fraud Decisioning" as Fraud
queue "Order Events" as Events
rectangle "Fulfillment" as Fulfillment
rectangle "Notification" as Notification
database "Support View" as Support

Buyer --> Checkout : place order
Checkout --> Pricing : price basket
Checkout --> Inventory : reserve
Checkout --> Payment : authorize
Checkout --> Fraud : risk gate
Checkout --> Events : OrderAccepted
Events --> Fulfillment
Events --> Notification
Events --> Support : project order state
BuyerCheckoutPricingInventoryPaymentFraud DecisioningOrder EventsFulfillmentNotificationSupport Viewplace orderprice basketreserveauthorizerisk gateOrderAcceptedproject order state

The review questions would include: Which rules must be immediately consistent? What is the latency budget? What happens when fraud is unavailable? How are duplicate payment attempts prevented? Can support see eventual states? Which team owns the checkout outcome? What event schemas are contractual? How are regional tax rules introduced without redeploying every service?

Case Study: SaaS Tenant Isolation

Consider a B2B SaaS platform moving upmarket. Enterprise customers require SSO, audit logs, role-based access, tenant-specific retention, data export, and stronger isolation. The key architectural question is not “single tenant or multi tenant?” It is what level of isolation each quality requires: identity, authorization, data storage, encryption keys, noisy-neighbor control, deployment, and operational access.

One design may keep shared application services but introduce tenant-scoped authorization, row-level security, per-tenant encryption keys, tenant-aware rate limits, and audit trails. Another may isolate high-value tenants into dedicated databases or clusters. The tradeoff is cost and operational complexity versus risk reduction and enterprise fit.

Code
left to right direction
actor "Enterprise User" as User
rectangle "SSO Identity" as SSO
rectangle "Tenant-Aware API" as API
rectangle "Authorization Policy" as Authz
database "Shared App Database\nrow-level tenant boundary" as SharedDB
database "Dedicated Tenant Database\nfor regulated customers" as DedicatedDB
rectangle "Audit Log" as Audit
rectangle "Key Management" as KMS

User --> SSO
SSO --> API : identity and tenant
API --> Authz : subject, action, tenant resource
API --> SharedDB : standard tenants
API --> DedicatedDB : isolated tenants
API --> Audit : immutable evidence
API --> KMS : tenant key
Enterprise UserSSO IdentityTenant-Aware APIAuthorization PolicyShared App Databaserow-level tenant boundaryDedicated Tenant Databasefor regulated customersAudit LogKey Managementidentity and tenantsubject, action, tenant resourcestandard tenantsisolated tenantsimmutable evidencetenant key

The review should ask: What tenant data can appear in logs, caches, analytics, and support tools? Can one tenant’s load harm another tenant? Who can access tenant data operationally? What is the migration path from shared to dedicated isolation? Which isolation decisions are contractual and which are implementation details?

Case Study: Legacy Modernization

Legacy modernization is rarely a rewrite. Rewrites are tempting because they promise a clean future, but they often fail because the old system encodes business rules nobody fully remembers. A safer architecture usually creates a strangler facade, extracts capabilities gradually, synchronizes or migrates data with reconciliation, and measures behavior equivalence.

The architectural challenge is coexistence. The old and new systems must share identity, routing, data, reporting, and operational support during migration. The design needs a decision for each capability: keep, wrap, replace, extract, or retire. It also needs a kill plan for migration scaffolding so the strangler does not become another permanent layer.

Code
left to right direction
actor "User" as User
rectangle "Routing Facade" as Facade
rectangle "Legacy Monolith" as Legacy
rectangle "New Account Service" as Account
rectangle "New Order Service" as Order
database "Legacy Database" as LegacyDB
database "New Stores" as NewStores
rectangle "Behavior Comparison" as Compare

User --> Facade
Facade --> Legacy : not migrated
Facade --> Account : migrated account capability
Facade --> Order : migrated order capability
Legacy --> LegacyDB
Account --> NewStores
Order --> NewStores
LegacyDB --> Compare
NewStores --> Compare
Compare --> Facade : confidence signal
UserRouting FacadeLegacy MonolithNew Account ServiceNew Order ServiceLegacy DatabaseNew StoresBehavior Comparisonnot migratedmigrated account capabilitymigrated order capabilityconfidence signal

Review Outcomes

A review should end with decisions, risks, owners, and next evidence. “Looks good” is not an outcome. Better outcomes include: approve the decision with documented tradeoffs; run a spike to validate latency; require a contract test before launch; split a boundary differently; add an operational control; defer service extraction; or create a migration checkpoint.

Architecture review is a learning loop. The best teams revisit decisions after incidents, scale changes, onboarding pain, security findings, or major product shifts. The review is not a gate at the beginning. It is a way to keep the architecture connected to reality.

Practice

Run a capstone review for one of the case studies or for a system you know. Prepare a one-page review packet with drivers, quality scenarios, boundaries, data ownership, runtime view, security view, tradeoffs, risks, and fitness functions. Then write the review outcome as an ADR with a review trigger.

References & Further Reading