3. Architectural Styles and System DecompositionIntroduction and ContextApplication architecture is the set of structural decisions that determine how software responsibilities are organized, how components communicate, how data is managed, and how the system is operated over time. In full stack development, architecture connects user interfaces, application services, domain rules, databases, external integrations, deployment infrastructure, security controls, and operational practices. It is therefore more than a diagram or a choice of programming framework. It is a set of constraints that shapes the system’s ability to evolve. An architectural style provides a recurring arrangement of responsibilities and interactions. Layered architecture, client–server architecture, modular monoliths, hexagonal architecture, clean architecture, microservices, serverless systems, and event-driven systems are not mutually exclusive technologies. They are design approaches that can be combined. For example, a microservice may use hexagonal internal structure, expose a client–server API, and publish domain events through an event-driven integration mechanism. The central architectural problem is decomposition: deciding where responsibilities should live and where boundaries should be drawn. Poor decomposition creates excessive coupling, duplicated logic, unclear ownership, and expensive change. Good decomposition supports high cohesion, controlled dependencies, independent reasoning, and an appropriate relationship between software boundaries and deployment boundaries. This chapter examines these choices comparatively and develops a practical method for selecting and evolving an architecture. Learning ObjectivesBy the end of this chapter, a student should be able to: - explain the defining characteristics of major architectural styles;
- distinguish logical modules from independently deployable services;
- analyse coupling, cohesion, communication, data ownership, and operational consequences;
- identify service boundaries using business capabilities and domain concepts;
- select an architecture in relation to scale, risk, team structure, compliance, and change;
- design evolutionary migration paths rather than treating architecture as a one-time decision;
- evaluate architectural risks, governance requirements, and professional trade-offs.
Architecture as a Set of BoundariesA useful way to understand architecture is to examine four kinds of boundary. A responsibility boundary separates what a component knows and does. A dependency boundary controls which components may call or depend on one another. A data boundary determines who owns and changes particular data. A deployment boundary determines what is released and operated independently. These boundaries may coincide, but they do not have to. A modular monolith may contain strong responsibility, dependency, and data boundaries while remaining one deployment unit. A microservice usually makes several boundaries independently deployable, but a poorly designed microservice system may still share databases and retain strong hidden coupling. The distinction is important: distributing code across processes does not automatically produce a well-decomposed system. Two related design qualities guide decomposition. Cohesion measures how closely related the responsibilities within a unit are. High cohesion means that a module or service represents a meaningful capability rather than a random collection of functions. Coupling measures the strength and number of dependencies between units. Lower coupling generally makes change safer, although eliminating all coupling is neither possible nor desirable. The objective is controlled coupling around stable contracts. A further distinction is between synchronous and asynchronous interaction. A synchronous request expects an immediate response and creates temporal dependency: the caller may be blocked while the receiver works. An asynchronous message or event allows the sender and receiver to progress more independently, but introduces eventual consistency, delivery concerns, and more complicated diagnosis. The Main Architectural StylesLayered ArchitectureLayered architecture organizes software into horizontal levels, commonly including presentation, application or service, domain, and infrastructure or data-access layers. An upper layer uses services offered by a lower layer. In a conventional web application, the browser communicates with controllers, controllers invoke application services, domain logic applies business rules, and repositories communicate with a database. The style is attractive because it is familiar, easy to teach, and compatible with most enterprise frameworks. It can separate technical concerns and provide a clear starting point for development. It is particularly suitable for systems with relatively stable workflows, moderate scale, and a need for straightforward maintenance. Its weakness is that layers can become merely technical folders. If business logic is scattered across controllers, services, and database procedures, the nominal layering does not provide meaningful modularity. Strict layering may also encourage every request to pass through every layer, even when this adds no value. A further risk is the big ball of mud, in which all layers depend indirectly on all others. Layering is best treated as an organizing principle rather than a guarantee of quality. Dependency rules should be explicit, and domain rules should not become dependent on web frameworks or database details merely because those details are convenient. Client–Server ArchitectureClient–server architecture separates request-initiating clients from servers that provide data or services. A client may be a browser, mobile application, desktop program, or another service. The server manages application processing, shared data, authentication, or other capabilities. This style supports centralized governance and enables several client types to use common server capabilities. Web APIs, database applications, and mobile back ends commonly follow it. The separation also permits clients and servers to evolve at different rates when contracts are carefully managed. However, the server can become a central bottleneck or a single conceptual unit containing unrelated responsibilities. Network calls introduce latency, availability risks, authentication requirements, and versioning problems. A client–server system can therefore be well structured or monolithic; the style alone does not determine internal decomposition. Modular MonolithA modular monolith is a single deployable application whose internal structure is divided into explicit modules. Each module owns a coherent business capability, exposes a limited interface, and avoids direct access to another module’s internal data or implementation. Modules may include customer management, order processing, billing, and fulfilment, while remaining within one runtime and release unit. This architecture is often an effective default for new products. It preserves simple deployment, local transactions, low-latency calls, and relatively easy debugging. At the same time, it establishes boundaries that may later support selective extraction into services. It also avoids the operational cost of distributing a system before there is evidence that distribution is necessary. The principal danger is gradual erosion. Developers may bypass interfaces, share tables, introduce circular dependencies, or place common code in an uncontrolled utility module. A modular monolith requires architectural tests, ownership rules, dependency analysis, and disciplined code review. Its single deployment unit also means that a change to one module may require testing and releasing the whole application. Hexagonal ArchitectureHexagonal architecture, also called ports-and-adapters architecture, places the domain and application core at the centre. The core defines ports, which are interfaces representing interactions it requires or provides. Adapters connect those ports to technologies such as HTTP, databases, message brokers, payment providers, and user interfaces. The key idea is that business rules should not depend directly on infrastructure. An inbound HTTP adapter can invoke an application use case through an input port. An outbound repository port can be implemented by a relational database adapter or an in-memory test adapter. Dependency direction points toward the core. Hexagonal architecture improves testability and protects domain logic from technical volatility. It is particularly useful when a system has several delivery mechanisms or external integrations. Its cost is additional abstraction: interfaces and adapters must be designed carefully rather than introduced mechanically. A trivial application can become unnecessarily elaborate if every class is wrapped in an abstraction without a meaningful variation or boundary. Clean ArchitectureClean architecture generalizes similar principles through concentric layers. Entities or domain rules are at the centre, use cases surround them, and interface adapters and frameworks occupy outer layers. The Dependency Rule states that source-code dependencies should point inward toward policies, not outward toward mechanisms. Clean architecture emphasizes use cases, dependency inversion, testability, and independence from frameworks, databases, and delivery technologies. It can be valuable in systems with complex business rules, long expected lifetimes, or multiple interfaces. In practice, clean and hexagonal architecture overlap substantially; both seek a stable core and replaceable external mechanisms. Their names describe different emphases rather than entirely different implementations. A common mistake is to interpret clean architecture as a fixed number of folders or layers. The important question is whether volatile technical decisions are prevented from controlling stable business policies. Microservices ArchitectureMicroservices architecture decomposes an application into independently deployable services, each aligned with a bounded business capability and responsible for its own behaviour and, ideally, its own data. Services communicate through APIs, messages, or events. They may be developed, scaled, and released by relatively autonomous teams. Microservices can provide independent scaling, fault isolation, technology flexibility, and team autonomy. They are useful when parts of a product have substantially different scaling profiles, release frequencies, reliability requirements, or ownership structures. They can also reduce the risk that a large organization must coordinate every change through one codebase and release pipeline. These advantages are accompanied by distributed-systems complexity. Network calls fail, messages may be duplicated or delayed, data becomes distributed, and end-to-end testing becomes harder. A transaction that was previously local may require a saga, compensation, or an explicitly eventual-consistency model. Observability, service discovery, secrets management, resilient communication, and incident response become architectural concerns. Microservices are not simply “small classes deployed separately.” A service boundary should correspond to a meaningful capability and an ownership model. Excessively small services create chatty communication and operational overhead, while excessively broad services reproduce monolith problems under distributed conditions. Serverless ArchitectureServerless architecture delegates much of the server provisioning and capacity management to a cloud provider. It commonly uses function-as-a-service for event-triggered computation, managed databases, object storage, queues, and API gateways. “Serverless” does not mean that servers do not exist; it means that their infrastructure management is abstracted from the application team. Serverless can reduce initial operational effort, support rapid experimentation, and scale particular functions according to demand. It is effective for asynchronous processing, scheduled work, lightweight APIs, file transformations, and variable workloads. Managed services can also accelerate delivery. Trade-offs include provider dependence, execution limits, cold-start latency in some environments, complex pricing, distributed debugging, and difficulty maintaining consistent local development. A function-based design can also become a distributed monolith if every function shares hidden state and must be released together. Serverless components still require sound domain boundaries, identity controls, observability, and lifecycle governance. Event-Driven ArchitectureEvent-driven architecture uses events—records of significant facts such as `OrderPlaced` or `PaymentAccepted—to coordinate components. Producers publish events without necessarily knowing every consumer. Consumers subscribe and react independently. Events may be transported through brokers or streaming platforms. This style supports temporal decoupling, extensibility, integration across bounded contexts, and asynchronous workload handling. A new consumer can often be added without changing the producer. It is especially useful for notifications, audit trails, analytics pipelines, workflow progression, and integration between independently owned capabilities. Event-driven systems require explicit event contracts, schema evolution, idempotent consumers, retry policies, dead-letter handling, ordering decisions, and correlation identifiers. The system may become eventually consistent, so user experiences and business processes must tolerate intermediate states. Events should not be confused with commands: an event states that something happened, whereas a command requests an action. Comparing Styles Through Decision ForcesNo architectural style is universally superior. Selection should begin with forces rather than fashion. Important forces include: - Business volatility: Which capabilities change most frequently, and which rules must remain stable?
- Scale and performance: Are workloads uniform, or do particular capabilities require independent scaling?
- Reliability: Should failure in one capability be isolated from others?
- Team topology: Can teams own a capability from development through operation?
- Data consistency: Are immediate transactions essential, or is eventual consistency acceptable?
- Delivery constraints: Is rapid deployment more important than independent release?
- Operational maturity: Can the organization support distributed tracing, on-call response, and platform automation?
- Regulation and security: Do data residency, auditability, segregation of duties, or access controls influence boundaries?
A layered modular monolith often offers the best balance for a small or medium product with one primary team. A modular monolith with hexagonal or clean internal structure is a strong evolutionary starting point when future uncertainty is high. Microservices become more defensible when independent scaling, team autonomy, fault isolation, or release independence are demonstrated needs. Serverless is attractive for variable or event-triggered workloads when managed-platform constraints are acceptable. Event-driven mechanisms are valuable when temporal decoupling and multiple consumers matter, but they should not replace synchronous interactions that require immediate validation or response. Finding Service BoundariesService boundaries should be discovered from business meaning, not from nouns in a database schema or from the size of source files. Domain-driven design offers useful concepts. A bounded context is a boundary within which terms, models, and rules have a consistent meaning. A business capability describes what the organization does, such as underwriting, inventory management, or subscription billing. These concepts help distinguish cohesive responsibilities. A practical boundary analysis can proceed as follows: 1. Identify major business capabilities, actors, workflows, and policy decisions. 2. Examine where terminology changes meaning. “Customer” in marketing may not mean the same thing as “customer” in fraud assessment. 3. Identify data ownership. A service should be authoritative for the data it governs, rather than allowing many services to update the same tables. 4. Map change coupling. If two areas must always change together, separating them may be premature. 5. Map runtime interaction. Frequent synchronous calls across a proposed boundary may indicate excessive distribution or an inappropriate boundary. 6. Assess non-functional differences such as security classification, scaling profile, availability, and latency. 7. Assign ownership to a team capable of operating the boundary. A useful test is whether a proposed service can make most of its important decisions using its own data and rules. Another is whether the service has a clear contract that can evolve without exposing internal tables. Boundaries are hypotheses and should be validated against real change patterns. Decomposition, Contracts, and DataAn API contract defines operations, representations, error semantics, authentication requirements, and compatibility expectations. A stable contract hides implementation details and permits independent evolution. Versioning is not merely placing `/v2` in a URL; it includes additive change, deprecation policy, consumer communication, and contract testing. Data ownership is equally important. Shared databases appear convenient but create hidden coupling: one team’s schema change can break another team, and ownership becomes ambiguous. Separate databases provide stronger autonomy but make joins, transactions, reporting, and migration more difficult. A pragmatic progression may begin with schema ownership inside a single database and move to separate storage only when the organizational and operational benefits justify it. Distributed transactions should not be introduced casually. Where a business process spans services, a saga coordinates local transactions and compensating actions. For example, a reservation may be created, payment authorized, and fulfilment confirmed; if fulfilment fails, the system may cancel the reservation or refund payment. Compensation is a business action, not a technical rollback, and may require human intervention. Evolutionary MigrationArchitecture should evolve through controlled steps. A common path is from a layered application to a modular monolith, then to selective service extraction. The first step is not rewriting everything; it is identifying modules, enforcing dependency direction, assigning data ownership, and introducing automated tests. Once a module has a stable contract and a justified operational reason for separation, it may be extracted. The strangler pattern places a new capability around or in front of a legacy system, gradually redirecting selected flows to the new implementation. An anti-corruption layer translates between old and new models. This avoids a high-risk “big bang” rewrite. Other useful techniques include branch by abstraction, which permits a new implementation behind an interface; dual reads during data migration; carefully controlled dual writes; and event publication from legacy transactions through an outbox. Migration should be measured by outcomes rather than by service count. Relevant measures include lead time for changes, deployment failure rate, recovery time, latency, availability, operational workload, defect rates, and team cognitive load. Extracting a service that does not improve any of these outcomes may merely increase complexity. Illustrative Case: Evolving an Online Retail PlatformBackgroundThe fictional company Northstar Retail operates an online store. Its initial system is a layered monolith with a web interface, application services, and a relational database. One development team owns the entire product. SituationThe company introduces mobile applications, a marketplace integration, and seasonal campaigns. The catalogue experiences heavy read traffic, while order processing and payment require stronger consistency. Releases are becoming slower because catalogue, checkout, and administration changes are tested and deployed together. Management proposes splitting the system into twenty microservices immediately. AnalysisThe proposal confuses deployment separation with useful decomposition. The team has limited operational experience with distributed systems, and the proposed services would share the same database. Catalogue browsing has a different scaling profile from checkout, but checkout, payment, and inventory contain closely related transactional rules. Notifications and analytics do not require the checkout request to wait synchronously. ApproachThe team first restructures the monolith into modules: Catalogue, Orders, Payments, Inventory, Accounts, and Notifications. Each module receives an explicit interface and owns its schema area. Architecture tests prevent direct access across module internals. The team introduces contract tests for mobile and marketplace APIs. Catalogue reads are then moved behind a cache and read-optimized representation. Notifications are changed to consume an `OrderConfirmed` event published through an outbox, so email and analytics failures do not block checkout. After measuring deployment and scaling constraints, the team extracts Catalogue because it requires independent scaling and has a clear API. Payment remains inside the monolith because its rules are tightly coupled to checkout and its extraction would add limited value. A later extraction of Notifications is performed as a serverless event consumer. OutcomeThe platform gains independent catalogue scaling and more resilient asynchronous processing without distributing every business rule. The organization acquires operational experience gradually. The remaining monolith retains clear modules and can be changed without treating it as an architectural failure. Learning PointsThe case demonstrates that architecture should follow evidence. A modular monolith can be an intentional and evolvable architecture. Independent deployment is valuable only when accompanied by ownership, data autonomy, suitable contracts, and operational capability. Different parts of one product may appropriately use different styles. Risks, Governance, and Professional PracticeDistributed architecture enlarges the failure surface. Partial failure, retries, duplicate messages, stale data, incompatible schemas, clock differences, and cascading overload must be considered. Timeouts, bounded retries, circuit breakers, bulkheads, idempotency keys, and dead-letter queues are useful controls, but each introduces policy decisions that must be tested. Security boundaries should align with data sensitivity and business responsibility. Every service or function requires authenticated identity, authorization, secrets protection, transport encryption, logging appropriate to privacy requirements, and controlled administrative access. Centralized identity may simplify governance, but services must still enforce authorization rather than trusting a caller’s claims blindly. Architecture governance should enable consistency without blocking responsible autonomy. Useful mechanisms include lightweight architecture decision records, published interface standards, automated dependency checks, threat modelling, service ownership registers, production readiness reviews, and regular technology-debt assessment. Governance must also address cost: cloud consumption, duplicated platforms, observability data, network traffic, and on-call demands are architectural consequences. Ethically, designers should consider availability for vulnerable users, protection of personal data, explainability of automated decisions, and the consequences of failure. Decomposition can accidentally fragment accountability. A user affected by an incorrect payment or access decision should not be passed between teams that each own only a technical fragment. Current Developments and Best PracticesContemporary architectures increasingly combine styles: modular domain cores with API delivery, event streams for integration, managed cloud services for infrastructure, and serverless components for variable workloads. Platform engineering seeks to provide reusable deployment, security, observability, and developer workflows so that product teams can own services without rebuilding infrastructure repeatedly. Service meshes can standardize communication concerns, although they add operational complexity and should not compensate for poor service boundaries. AI-assisted development increases the speed at which code and services can be produced, but it does not remove the need for architecture. Automatically generated code can duplicate business rules, expose sensitive data, or create inconsistent contracts. Architecture tests, code review, threat modelling, and ownership remain necessary. Sound practice can be summarized in several principles: begin with the simplest architecture that satisfies known constraints; model business capabilities and data ownership; keep modules cohesive; make dependencies explicit; prefer contracts over shared internals; use asynchronous communication for genuine temporal decoupling; design for observability; automate testing and deployment; record consequential decisions; and evolve boundaries in response to evidence. Chapter SummaryArchitectural styles provide alternative ways to organize responsibility, communication, data, and deployment. Layered and client–server styles offer familiar foundations; modular monoliths provide strong internal organization without distributed operational cost; hexagonal and clean architectures protect business rules from infrastructure; microservices support selective independent deployment at the cost of distributed-systems complexity; serverless delegates infrastructure management; and event-driven architecture enables asynchronous, extensible interaction. The decisive issue is not selecting the most fashionable style but drawing appropriate boundaries. Cohesion, controlled coupling, business capabilities, bounded contexts, data ownership, team ownership, consistency needs, operational maturity, and non-functional requirements should guide decomposition. A modular monolith can evolve into selected services through strangler and anti-corruption patterns, while metrics validate whether migration produces genuine value. Good architecture remains a managed set of trade-offs, supported by contracts, observability, security, governance, and continuous learning. Key TermsArchitectural style: A recurring structural approach for organizing components and their interactions. Cohesion: The degree to which responsibilities within a module or service belong together. Coupling: The strength of dependency between software units. Layered architecture: An arrangement of software into horizontal levels with defined dependency directions. Client–server architecture: A style separating request-initiating clients from service-providing servers. Modular monolith: A single deployment unit containing internally isolated, cohesive modules. Port: An interface representing an interaction with an application or domain core. Adapter: An implementation that connects a port to an external technology or system. Hexagonal architecture: An architecture that isolates a core through ports and adapters. Clean architecture: An architecture applying inward dependency rules to protect policies and use cases. Microservice: An independently deployable component aligned with a bounded business capability. Serverless: A cloud execution model in which infrastructure provisioning is largely managed by a provider. Event: A record that a significant fact has occurred. Bounded context: A boundary within which domain terms and models have consistent meaning. Data ownership: The authority and responsibility of a component or team to manage particular data. Saga: A sequence of local transactions coordinated across services with compensating actions. Strangler pattern: An evolutionary migration approach that gradually replaces parts of a legacy system. Anti-corruption layer: A translation boundary that prevents an external or legacy model from contaminating a new domain model. Review QuestionsShort Answer Questions1. Distinguish a logical module from a deployment unit. 2. What is the purpose of a port in hexagonal architecture? 3. State two benefits and two costs of microservices. 4. Why is data ownership important in service decomposition? 5. Explain the difference between an event and a command. Long Answer Questions1. Compare layered architecture, modular monoliths, and microservices in terms of coupling, deployment, data, and operational complexity. 2. Explain how bounded contexts and business capabilities can guide service boundaries. 3. Discuss the dependency rule in clean architecture and its relationship to hexagonal architecture. 4. Describe an evolutionary migration from a legacy monolith using the strangler pattern and an anti-corruption layer. 5. Analyse the organizational and governance conditions required for successful microservices adoption. Analytical / Application Questions1. A university system has one development team, stable workloads, complex enrolment transactions, and frequent reporting changes. Which architecture would you recommend initially, and why? 2. An online platform has independent teams for search, payment, and fulfilment, but all services share one database and call each other synchronously for every request. Identify the architectural risks and propose a staged improvement plan. 3. A retailer needs to process image resizing and send customer notifications after orders are placed, but neither task should delay checkout. Which architectural styles or mechanisms would you use, and what failure-handling policies are necessary? 22. Multiple Choice Questions with AnswersQuestion 1Which characteristic most clearly distinguishes a modular monolith from an unstructured monolith? A. It always uses microservices for external integrations. B. It has explicit internal module boundaries and controlled dependencies. C. It stores every module in a separate database server. D. It requires asynchronous communication between all modules. Correct Answer: B. It has explicit internal module boundaries and controlled dependencies. Question 2In hexagonal architecture, why are adapters placed outside the application core? A. To ensure that business rules depend on replaceable technical mechanisms. B. To prevent the application from having any external interface. C. To allow infrastructure details to depend on stable core policies. D. To eliminate the need for automated testing. Correct Answer: C. To allow infrastructure details to depend on stable core policies. Question 3Which situation provides the strongest justification for extracting a module into a microservice? A. The module has a different scaling profile and a clear independently owned contract. B. The module contains only a few classes. C. The team wants to increase the number of repositories. D. The module uses the same database tables as every other module. Correct Answer: A. The module has a different scaling profile and a clear independently owned contract. Question 4What is a major consequence of replacing a local transaction with interactions across services? A. All failures disappear because services are isolated. B. The system may require coordination, compensation, or eventual consistency. C. Database ownership becomes irrelevant. D. Network latency is eliminated. Correct Answer: B. The system may require coordination, compensation, or eventual consistency. Question 5Which statement correctly describes an event-driven interaction? A. An event is always a synchronous request for immediate action. B. An event records that something happened and may be consumed asynchronously. C. An event must expose the producer’s database schema. D. An event can have only one consumer. Correct Answer: B. An event records that something happened and may be consumed asynchronously. Question 6A proposed service requires synchronous calls to four other services for nearly every operation. What does this most likely indicate? A. Perfect temporal decoupling. B. A potentially poor boundary or excessive distributed coupling. C. That the service has complete data autonomy. D. That event-driven architecture is impossible. Correct Answer: B. A potentially poor boundary or excessive distributed coupling. Question 7What is the primary purpose of an anti-corruption layer during migration? A. To remove all legacy code immediately. B. To translate between legacy and new models while protecting the new design. C. To make every service share the same domain model. D. To replace authentication with network location. Correct Answer: B. To translate between legacy and new models while protecting the new design. Question 8Which workload is particularly suitable for a serverless function? A. A highly stateful process requiring unlimited execution time and strict local transactions. B. An event-triggered image transformation with variable demand. C. A database schema shared directly by many unrelated teams. D. A business rule that must be duplicated in every client. Correct Answer: B. An event-triggered image transformation with variable demand. Question 9Which factor should most strongly influence whether a system adopts microservices? A. The popularity of microservices in industry articles. B. The number of classes in the existing codebase. C. Evidence of required independent scaling, release, ownership, or fault isolation. D. A desire to avoid writing automated tests. Correct Answer: C. Evidence of required independent scaling, release, ownership, or fault isolation. Question 10In the Northstar Retail case, why did the team retain payment inside the monolith initially? A. Payment had no business rules. B. Payment was tightly coupled to checkout and extraction offered limited demonstrated value. C. Payment could not be tested inside a monolith. D. Payment required event-driven communication for every operation. Correct Answer: B. Payment was tightly coupled to checkout and extraction offered limited demonstrated value.
|