Before the questions, make sure you can: explain what architecture is and why its decisions are expensive to change; judge a design by coupling and cohesion and apply Parnas's information hiding; use the SOLID principles — especially dependency inversion — to isolate things that will change; describe layered, client–server, MVC, event-driven, modular-monolith and microservice styles and choose between them from quality attributes and team size; apply tactics for availability and performance (timeouts, retries with back-off, circuit breakers, fallbacks, caching); compute simple coupling metrics (fan-in, fan-out, instability) and find dependency cycles and layer violations; write an architecture decision record (ADR); and design an AI-based feature as an unreliable external dependency with a provider interface, retrieval-augmented generation (RAG), guardrails, fallbacks and evaluation hooks — and critique over-engineered architectures suggested by AI tools.
Chapter 1 showed that most of the cost of software comes after the first release, when it must change. Design is how you make future change cheap. Architecture is the set of decisions that are hardest to change later — how the system is split into parts, how the parts talk, where data lives. Good architecture does not predict the future; it isolates the things most likely to change so that changing them does not break everything else. For StudyBuddy, one thing is certain to change: the AI model behind Buddy.
Architecture: the expensive decisions
Grady Booch described architecture as the significant design decisions, where significance is measured by the cost of change. Examples for StudyBuddy:
| Decision | Cost to change later |
|---|---|
| One application vs. many services | Very high |
| Relational database (MySQL) vs. document store | High |
All AI calls go through our own TutorService interface |
Low to add now, very high to add later |
| The colour of the "Post" button | Almost zero — not architecture |
Architecture is driven mostly by quality attributes (Chapter 1), not by features: almost any architecture can implement "post a question", but only some can do it with 99.5% availability, answers in 5 s, strong privacy and a team of five students.
Coupling and cohesion
Two classic measures of a design:
- Coupling — how much one module depends on the internals of another. Aim low. With low coupling, changing one module rarely forces changes in others.
- Cohesion — how strongly the parts inside one module belong together. Aim high. A cohesive module has one clear job.
A good kitchen has high cohesion (all baking tools in one drawer) and low coupling (you can replace the oven without rebuilding the fridge). A bad kitchen has spoons in every drawer and an oven wired into the fridge.
Signs of high coupling: a class that reads another class's fields directly; a change to the database table that breaks the Android screens; a module that imports ten others. Signs of low cohesion: a class called Utils or Manager with forty unrelated methods.
Information hiding (Parnas, 1972)
David Parnas's paper "On the Criteria To Be Used in Decomposing Systems into Modules" gave the most important rule of modular design: each module should hide a design decision that is likely to change, behind an interface that is unlikely to change.
For StudyBuddy, likely changes include: the AI provider and model, the way course files are split and searched, the notification channel (push, email, WeChat), the database schema. Each should be hidden inside one module. The rest of the system talks to TutorService.answer(question, course), not to "vendor X's HTTP API with model Y and temperature 0.2".
SOLID, briefly (from CPS 3962)
| Principle | Meaning | StudyBuddy example |
|---|---|---|
| Single responsibility | A class has one reason to change | PostValidator checks posts; it does not also send emails |
| Open/closed | Open for extension, closed for modification | Add a new notification channel by adding a class, not by editing a big switch |
| Liskov substitution | Subtypes must work wherever the base type is expected | Every Tutor implementation returns an answer or throws TutorUnavailableException — none returns null silently |
| Interface segregation | Many small interfaces beat one huge one | Searchable and Moderatable instead of EverythingService |
| Dependency inversion | High-level code depends on abstractions, not on concrete low-level details | QuestionController depends on the Tutor interface, not on VendorXClient |
Dependency inversion is the key tool for "design for change":
public interface Tutor {
Answer answer(String question, CourseId course) throws TutorUnavailableException;
}
public class VendorXTutor implements Tutor { /* HTTP calls to provider X */ }
public class VendorYTutor implements Tutor { /* HTTP calls to provider Y */ }
public class FakeTutor implements Tutor { /* fixed answers, for tests */ }
public class QuestionController {
private final Tutor tutor; // depends on the abstraction
public QuestionController(Tutor tutor) { // the concrete class is chosen at start-up
this.tutor = tutor;
}
}
Now the provider can change in one class, the controller can be tested with FakeTutor (no network, no cost, deterministic), and a fallback wrapper can be added without touching callers.
Architectural styles
| Style | Idea | Strength | Weakness |
|---|---|---|---|
| Layered | Presentation → application/service → domain → data access; each layer uses only the layers below | Simple, familiar, easy to test layers | Changes to one feature cross all layers; "layer-skipping" erodes it |
| Client–server | Clients (web, Android) call a server API | Central data and rules | Server is a single point of failure unless replicated |
| MVC | Model (data/rules), View (display), Controller (input) | Separates UI from logic | Controllers tend to grow fat |
| Event-driven | Components publish events ("PostReported"); others react | Loose coupling, easy to add reactions | Harder to follow the flow and debug |
| Modular monolith | One deployable application, split internally into well-separated modules | Simple to deploy and test, fast calls, one database | Needs discipline to keep modules separate |
| Microservices | Many small services, each deployed and scaled independently, each owning its data | Independent teams and scaling | Network failures, distributed data, operations cost; needs many people |
Microservices solve an organizational problem: many teams that must deploy independently (Conway's law, Chapter 5). A team of five students with microservices gets all the costs — network failures, distributed transactions, many deployments, monitoring — and none of the benefits. For StudyBuddy a modular monolith with clear module boundaries (and the AI calls behind an interface) is the sensible choice; a module can be extracted into a service later if a real need appears.
StudyBuddy's architecture (simplified):
Android app ──┐
├──► REST API (Spring Boot, modular monolith) ──► MySQL
Web app ──────┘ │ modules: accounts, courses, board,
│ matching, moderation, tutor, notifications
└──► tutor module ──► TutorService ──► external AI provider
└──► course-file index (retrieval)
Tactics for quality attributes
Architects use known tactics to reach quality targets:
Availability and resilience (especially toward external services like the AI provider):
- Timeouts — never wait forever: "give up after 10 s".
- Retries with back-off — retry a failed call after 1 s, then 2 s, then 4 s (and only for errors that may be temporary); never retry forever.
- Circuit breaker — after several consecutive failures, stop calling the failing service for a while ("open" the circuit) and fail fast; then let one trial call through ("half-open"); close the circuit again when it succeeds. This protects both your users (no long waits) and the failing service (no flood of retries).
- Fallback / graceful degradation — when Buddy is down, the board still works and offers "post your question to classmates".
- Redundancy — two servers, or a second AI provider.
Performance and cost:
- Caching — keep recent answers to identical questions ("What is polymorphism?" asked 40 times before the midterm) for a limited time (time-to-live, TTL), with an eviction policy such as least-recently-used (LRU).
- Asynchronous processing — generate summaries or send notifications in the background, not while the user waits.
Security (Chapter 10): least privilege, validating input at the boundary, keeping secrets (API keys) out of the code.
Maintainability: modules with clear interfaces, no dependency cycles, dependencies pointing in one direction (toward stable abstractions).
Measuring structure: fan-in, fan-out, instability, cycles
For each module:
- Fan-in (afferent coupling, Ca) — how many modules depend on it.
- Fan-out (efferent coupling, Ce) — how many modules it depends on.
- Instability I = Ce / (Ca + Ce) (Robert C. Martin) — 0 means maximally stable (many depend on it, it depends on nothing); 1 means maximally unstable (it depends on others, nobody depends on it).
The stable dependencies principle: depend in the direction of stability. A module many others rely on should be stable (and usually abstract, like the Tutor interface). Cycles (A → B → C → A) are especially harmful: none of the modules can be understood, tested or changed alone. Tools can check these rules automatically in CI.
Architecture decision records (ADRs)
Decisions are forgotten; their reasons even faster. An ADR (Michael Nygard, 2011) is a short text file kept in the repository, one per important decision:
ADR-003: Put all AI calls behind a TutorService interface
Status: Accepted (2026-09-30)
Context: Buddy uses an external AI provider. Prices, limits and models change often;
the university may require a provider hosted in China; tests must not call the network.
Decision: All AI access goes through the Tutor interface in the tutor module.
Providers are adapters; the active one is chosen by configuration.
Consequences: + provider can be switched in one class; + FakeTutor for tests;
+ one place for timeouts, circuit breaker, logging and cost limits.
− an extra layer to maintain; − provider-specific features need a
deliberate extension of the interface.
An ADR is never edited to change history: a new ADR supersedes the old one.
The AI angle 1: designing around an AI component
Treat a large-language-model (LLM) service as an unreliable, expensive, non-deterministic external dependency:
- Provider interface (dependency inversion) — switch or combine providers; test with a fake.
- Retrieval-augmented generation (RAG) — instead of hoping the model "knows" the course, Buddy first retrieves the most relevant passages from the instructor's uploaded files (a search index), then asks the model to answer only from those passages and to cite them. This makes answers grounded, checkable (citations) and course-specific, and keeps the course files under the university's control.
- Guardrails — checks before the call (block personal data, detect graded exercises) and after the call (does the answer cite a retrieved source? does it contain a full solution to a graded exercise?).
- Resilience tactics — timeout, limited retries, circuit breaker, fallback message, cache for repeated questions, a monthly cost limit.
- Configuration, not code — the prompt template and model name are versioned configuration, so changing them is a controlled release (remember CrowdStrike: "just configuration" can break everything).
- Evaluation hooks — log (without personal data) which sources were used and let users rate answers, so quality can be measured (Chapter 9).
question ─► guardrail (PII, graded?) ─► retrieve top-k passages ─► prompt(passages, question)
─► Tutor (timeout, breaker, fallback) ─► guardrail (citations, no full solution)
─► answer + sources ─► cache / feedback log
The AI angle 2: AI as a design assistant
AI tools are useful for exploring options ("list three ways to structure notifications and their trade-offs"), explaining patterns, and drafting ADRs. Their typical failure: fashionable over-engineering. Ask "design the architecture for a student Q&A app" and you may get nine microservices, a message broker, Kubernetes and a separate database per service — the architecture of a large company, copied from blog posts. The engineer's job is to judge proposals against this project's quality attributes, team size, skills, budget and hosting constraints — and to write down why in an ADR.
Architecture questions rarely have one right answer. Strong answers name the quality attributes at stake, compare at least two options, state the trade-off, and connect the decision to the context (team size, change likelihood, constraints).
Key takeaways
- Architecture = the decisions that are expensive to change; it is driven by quality attributes.
- Aim for low coupling, high cohesion; hide each likely-to-change decision inside one module (Parnas).
- Dependency inversion: high-level code depends on interfaces (
Tutor), not on concrete providers — enabling change, fakes for tests, and resilience wrappers. - Styles: layered, client–server, MVC, event-driven, modular monolith, microservices — choose by quality attributes and team structure; microservices solve an organizational problem.
- Tactics: timeouts, retries with back-off, circuit breaker, fallback, redundancy, caching with TTL/LRU, async work.
- Measure structure: fan-in, fan-out, instability = Ce / (Ca + Ce); avoid cycles and layer violations.
- Record decisions in ADRs (context, decision, consequences; superseded, never rewritten).
- Treat AI services as unreliable dependencies: provider interface, RAG with citations, guardrails, resilience, versioned prompts, evaluation hooks; beware AI-suggested over-engineering.
Ready? Close the notes and practise.
30 questions. Predict the output before you check — that is the skill the exam measures.