We reach the end of the course, and nothing consolidates what has been learned better than designing a complete system from scratch, applying—with justification—the decisions we have been studying. In this lesson we will act as the architecture team of a fictional company, MercadoFiatc, which needs to build an e-commerce platform. We will start from the business requirements, translate them into quality attributes, choose an architectural style with its justification, model the data, define deployment and observability, and record the key decisions as ADRs. We are not looking for "the" correct answer—in architecture it almost never exists—but rather to show coherent reasoning: each decision derives from a requirement and consciously accepts its trade-offs. This case closes the course by integrating the modules on styles, data, communication, deployment, governance, and evolution.
Contents
- MercadoFiatc's requirements
- From requirements to quality attributes
- Choosing the architectural style
- C4 diagram of the system
- Data design
- Communication between components
- Deployment
- Observability
- Recorded decisions (ADR) and evolution
- Common Mistakes and Tips
- Exercises
- Course conclusion
- MercadoFiatc's requirements
The business presents us with these functional requirements and constraints:
- A browsable product catalog with search, tens of thousands of references.
- A shopping cart and payment process (checkout) with an external gateway.
- Order management and its status (confirmed, preparing, shipped, delivered).
- Product recommendations.
- Very strong traffic peaks during campaigns (Black Friday): catalog browsing can multiply by 50, while payments grow much less.
- A team of about 25 people, organized into 4 squads.
- A limited operating budget: they do not want to pay the complexity of a distributed system where it does not add value.
- From requirements to quality attributes
The first step of any serious architecture is to translate business requirements into measurable quality attributes, because it is they—and not the features—that most condition the design.
| Business requirement | Quality attribute | Measurable objective |
|---|---|---|
| Campaign peaks in catalog | Scalability (granular) | Catalog scales x50 without touching payments |
| Payment cannot go down | Availability | 99.95% on checkout |
| Smooth browsing | Performance | p95 of catalog < 200 ms |
| A team of 4 autonomous squads | Maintainability / autonomy | Squads deploy without coordinating |
| Limited budget | Operating cost | Minimize distributed pieces |
| Reliable order data | Consistency | Order and payment consistent (transactional where needed) |
Notice the key tension: we need granular scaling (catalog grows much more than payments) and squad autonomy, which pushes toward microservices; but the limited operating cost and the order's consistency push toward the monolith. The architecture will be born from balancing those forces.
- Choosing the architectural style
We do not jump straight to "microservices for everything." We evaluate options:
| Style | In favor for MercadoFiatc | Against |
|---|---|---|
| Modular monolith | Low cost, easy consistency, cheap refactoring | Does not allow scaling the catalog separately or total squad autonomy |
| Full microservices | Granular scaling and maximum autonomy | High operating cost, complex consistency, over-engineering at the start |
| Mixed: modular monolith + a few extracted services | Captures scaling where it matters, without paying the full cost | Requires boundary discipline |
We choose the mixed and evolutionary approach, fully aligned with what we have learned: we start with a modular monolith well delimited by domains (Catalog, Cart, Orders, Payments, Recommendations) and extract as a microservice only what has disparate scaling or availability needs. Specifically:
- Catalog is extracted early: it is the one that suffers the x50 during a campaign and is worth scaling on its own.
- Recommendations is extracted: it is optional, can fail without breaking the purchase, and evolves quickly.
- Cart, Orders, and Payments remain together in the modular monolith at first, because they share transactions and strong consistency; extracting them early would bring sagas and pain without a clear benefit.
This decision directly applies the MonolithFirst rule and the Strangler Fig pattern: the modular monolith is the starting point, and extraction is incremental and guided by real pain.
- C4 diagram of the system
We use the C4 model (Context, Containers, Components, Code) to communicate the design to different audiences. We show the Containers level, the most useful one for architecture.
graph TD
Cliente[Web/mobile client] --> GW[API Gateway / Strangler Facade]
GW --> Cat[Catalog service\nextracted, scales x50]
GW --> Rec[Recommendations service\nextracted, optional]
GW --> Mono[Modular Monolith\nCart + Orders + Payments]
Mono --> Pasarela[External payment gateway]
Cat --> DBCat[(Catalog DB)]
Rec --> DBRec[(Recommendations DB)]
Mono --> DBMono[(Main DB:\nCart, Orders, Payments)]
Mono -->|OrderConfirmed event| Broker[(Messaging broker)]
Broker --> RecLet's read the diagram, which synthesizes almost all the decisions:
- The API Gateway also acts as a Strangler Facade: it routes each request to the extracted service or to the monolith. It is the point from which we will keep strangling if needed.
- Catalog and Recommendations are independent containers with their own database: they can be scaled and deployed separately.
- The modular monolith groups Cart, Orders, and Payments over a single database, allowing local ACID transactions during checkout.
- Communication with Recommendations is asynchronous via events (
OrderConfirmedvia broker): this way the order does not depend on Recommendations being available.
- Data design
We apply the principle of one database per service where there are separate services, and strong consistency where the transaction requires it:
| Component | Store | Justification |
|---|---|---|
| Catalog | An engine with search (e.g., PostgreSQL + text index, or Elasticsearch for advanced search) | Massive reads and search; tolerates read replicas |
| Recommendations | Its own store (may be NoSQL) | Derived data, flexible schema, non-transactional |
| Cart + Orders + Payments | PostgreSQL (relational, ACID) | Checkout crosses the three entities and requires a strong transaction |
The most delicate decision is the consistency between order and payment. Since they live in the same monolith over the same relational DB, confirming the order and recording the payment happens in a single local ACID transaction: either everything is done or nothing is. This avoids distributed sagas, which we would only introduce if we later extracted Payments. The relationship with Recommendations, on the other hand, is eventual consistency: after confirming the order an event is published and Recommendations is updated with a slight delay, which is perfectly acceptable for suggested data.
- Communication between components
We combine communication styles according to the nature of each interaction:
- Synchronous (REST/HTTP) through the gateway for client requests: browsing the catalog, viewing the cart, checking out. The user expects an immediate response.
- Asynchronous via events for internal integrations that tolerate delay:
OrderConfirmedtriggers the update of Recommendations and, in the future, of Billing. - Anti-Corruption Layer in the Catalog service if it were necessary to integrate with a legacy PIM (Product Information Management) with an old data model.
# Contract of the domain event published when confirming an order
event: OrderConfirmed
version: 1
payload:
orderId: "uuid"
customerId: "uuid"
lines:
- productId: "uuid"
quantity: 2
total: 149.90
currency: "EUR"
date: "2026-06-30T10:15:00Z"This event contract is the public boundary between the monolith and the consumers (Recommendations today, Billing tomorrow). Defining it explicitly and versioned (version: 1) is essential: consumers depend on its shape, so an incompatible change would require a version: 2. The event carries only what is necessary and does not include sensitive personal data beyond identifiers, which also helps respect data minimization.
- Deployment
The deployment reflects the mixed architecture and the contained budget:
- Containers (Docker) orchestrated with Kubernetes, but with few pieces: monolith, catalog, recommendations, gateway, and broker. We do not multiply services unnecessarily.
- Independent horizontal scaling: Catalog is configured with aggressive autoscaling (many replicas during a campaign); the monolith and the rest, with more moderate scaling.
- Independent deployment per squad: each container has its own pipeline, so that the Catalog squad deploys without coordinating with the Orders squad. This satisfies the autonomy attribute.
- Progressive deployment strategy (canary or blue-green) to reduce the risk in each release, with the possibility of rolling back.
# Outline of the Catalog service's autoscaling (Kubernetes HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: catalog-hpa
spec:
scaleTargetRef:
kind: Deployment
name: catalog
minReplicas: 3
maxReplicas: 60 # headroom for the campaign x50 peak
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65This manifest materializes the granular scalability requirement: the catalog can go from 3 to 60 replicas automatically when the CPU exceeds 65% usage, absorbing the campaign peak, while the rest of the system maintains its usual capacity. It is exactly the benefit that justified extracting Catalog as a separate service.
- Observability
A system with distributed pieces and asynchronous events is opaque without observability. We implement the three pillars:
- Structured logs (JSON) centralized, with a common
traceIdacross all pieces. - Metrics (Prometheus/Grafana): p95 latency per service, error rate, broker queue depth, active replicas.
- Distributed traces (OpenTelemetry): follow a checkout request through the gateway, the monolith, and the broker, seeing where the time goes.
Observability is not a luxury: it is the prerequisite for evolving safely. Without catalog latency metrics, we would not know whether autoscaling works; without traces, debugging the Recommendations event flow would be guesswork. Moreover, the metrics feed continuous fitness functions: an alert if the checkout p95 exceeds 300 ms is a fitness function that watches the architectural health in production.
- Recorded decisions (ADR) and evolution
We close the design by documenting the key decisions as ADRs, just as we saw in the governance lesson:
- ADR-001: Start with a modular monolith and extract services only out of pain (scaling/availability). Consequence: low initial cost; we accept refactoring boundaries later.
- ADR-002: Extract Catalog as an independent service. Consequence: granular scaling; the cost of its own DB and pipeline.
- ADR-003: Asynchronous event-based communication toward Recommendations. Consequence: decoupling and availability; eventual consistency and mandatory idempotency.
- ADR-004: Keep Cart, Orders, and Payments together with a local ACID transaction. Consequence: strong consistency without sagas; re-evaluate if Payments needs to scale separately.
And we protect the architecture with fitness functions: ArchUnit tests that forbid cycles between the monolith's modules and that prevent the domain from depending on the infrastructure, plus a continuous latency fitness function in production. This way, the architecture is documented (ADR) and monitored (fitness functions): it can evolve without degrading. When a module of the monolith starts to hurt, the Strangler Facade is already in place to extract it incrementally.
- Common Mistakes and Tips
- Starting with the solution and not with the quality attributes. Designing "with microservices" before knowing what needs to scale is putting the answer before the question.
- Extracting services out of fashion. Each extracted service costs operations, observability, and consistency. Extract only where the quality attribute justifies it (Catalog yes, Payments not yet).
- Forgetting consistency when separating. The moment you split a transaction across services, you enter sagas and eventual consistency. Be clear about this before cutting.
- Designing without observability. A mixed system without traces or metrics is impossible to evolve safely.
- Not documenting the decisions. Without ADRs, a year from now nobody will remember why Payments is in the monolith and someone will "fix it," creating a mess.
- Tip: check that each design decision can be traced back to a specific requirement or quality attribute. If you cannot, it is probably superfluous.
- Exercises
Exercise 1. The business adds a requirement: "sending transactional emails (order confirmation) must not slow down checkout and must be retryable if the email provider fails." How would you integrate it into the proposed architecture?
Exercise 2. After six months, the metrics show that Payments suffers its own peaks and the team wants to deploy it separately. Describe, using what you have learned, how you would extract it from the monolith and what new consistency problem appears.
Exercise 3. Justify why Catalog uses a search- and read-oriented store, while Orders uses a relational ACID one. Relate each choice to a quality attribute.
Solutions
Solution 1. Through asynchronous event-based communication: checkout publishes OrderConfirmed (which already exists) and a notifications consumer sends the email in a deferred manner. This way checkout does not wait for the email; if the provider fails, the consumer retries from the queue. Idempotency is advisable to avoid sending the email twice if the event is redelivered.
Solution 2. By applying Branch by Abstraction inside the monolith (a PaymentsService interface), then building the Payments microservice with its own DB and switching with feature flags via the gateway (Strangler). The new problem: order confirmation and payment recording no longer share an ACID transaction; the need arises for a saga with compensation (e.g., cancel the order if the payment fails) and eventual consistency between the two.
Solution 3. Catalog prioritizes read performance and scalability (fast search over tens of thousands of references under x50 peaks), hence a store optimized for search and replicas. Orders prioritizes consistency: checkout crosses cart, order, and payment in an operation that must be all-or-nothing, which requires the ACID transactions of a relational store.
- Course conclusion
With this case study we close the Application Architecture course. We have seen how a good design does not come from choosing the trendy technology, but from disciplined reasoning: translating business requirements into measurable quality attributes, choosing the style that best balances them—at MercadoFiatc, a modular monolith that evolves by extracting only what justifies it—modeling the data according to consistency needs, combining synchronous and asynchronous communication, deploying with granular scaling where it matters, and sustaining it all with observability. And, above all, we have closed the circle of the final module: documenting the decisions with ADRs, protecting them with fitness functions, and leaving the Strangler Facade ready to keep evolving incrementally and in a guided way.
Throughout the course you have walked the complete path: what architecture is, monolithic and distributed styles, layers and hexagonal architecture, data management and communication, deployment, and finally evolution and governance. The underlying lesson is always the same: in architecture there are no perfect solutions, only well-reasoned trade-offs. Your job as an architect is not to eliminate the trade-offs, but to choose them consciously, justify them, and leave the system ready to change when today's trade-offs stop serving. Congratulations on completing the course.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
