Software Architecture, Just Enough
Week 6 · Decide what is expensive to change
Week 6 · Decide what is expensive to change
Both deliver every use case. Both pass every functional test. Which one should the client get?
Team B is Project Pulse. Team A is what an agent proposes when nobody tells it those four answers.
Quality attributes tell you how to arrange them.
The significant design decisions that shape a system, where significant is measured by cost of change.
Grady Booch
| Decide now | Decide later, against real code |
|---|---|
| One deployable or several | Class and method names |
| Where the data lives | Endpoint shapes |
| How users sign in | Table columns |
| Which external systems | Validation library |
| How code is divided into modules | Screen layout |
| Where the trust boundary sits | Error wording |
Breadth-complete
Every use case area, every component, every external system named. No holes in the map.
Depth-shallow
Each part described to its responsibility and no further. Nothing designed before anyone has built against it.
The map with holes: two people build the same thing. The map that is too deep: you committed to guesses.
| Attribute | Project Pulse says | So the architecture has |
|---|---|---|
| Security | a student sees only their own team's work | a trust boundary; every query scoped |
| Maintainability | a new feature edits no sibling module | packages by domain |
| Availability | up 99% of a term | one instance, and that is enough |
| Performance | 500 ms at the 95th percentile | no needless network hops |
| Scalability | about 75 users, 100 at once | one deployable; uploads in object storage |
| Robustness | a crash loses at most 10 seconds | a save to the server at least every 10 seconds |
| Operability | one instructor, no operations team | one container, a staging slot |
An architecture that maximizes every quality attribute is like a car that is the fastest, the cheapest, and the safest. Ask to see it.
The few where a wrong guess costs a redesign, not a bug fix.
Rank each on two axes:
Importance to the client
A privacy breach: high. 1.5 seconds instead of 1: low.
Difficulty to achieve
75 users: low. 75,000 concurrent: high.
| Rank | Requirement | Handles | Drives |
|---|---|---|---|
| 1 | Student records stay confidential (FERPA) | SEC-authorization, SEC-ferpa, CO-ferpa |
KD-ram-module, KD-self-issued-jwt |
| 2 | One instructor, no operations team | AVL-uptime, CO-no-ops-team |
KD-modular-monolith, KD-relational-graph |
| 3 | Next year's students can extend it | MNT-feature-locality, MNT-service-layer |
KD-ram-module, KD-no-codegen, KD-vertical-slices |
| 4 | No lost work under concurrent editing | ROB-no-overwrite |
KD-section-locking |
Reuse the identifiers you already have. Include at least one SEC-*.
You write
1.2 Quality goals · 3 Context · 4 Solution strategy · 5 Building blocks · 6 Runtime · 7 Deployment · 8 Crosscutting · 9 Decisions · 10.2 Quality scenarios · 11 Technical risks
You link
1.1 Requirements · 1.3 Stakeholders · 2 Constraints · 10.1 Quality attributes · 12 Glossary
A fact written in two places is soon wrong in one.
| Template section | Answers | Drawn at |
|---|---|---|
| 3. Context and Scope | What is it, who uses it, what does it talk to? | Checkpoint 1 |
| 5. Building Block View | What is it made of, and who owns what? | Checkpoint 1 |
| 6. Runtime View | How do the parts carry out one use case? | Checkpoint 2 |
| 7. Deployment View | Where does it run, and how does a change get there? | Checkpoint 3 |
arc42: what to write · C4: how to draw it
Now, from the specification
Context. Building blocks. These are the decisions you are making this week.
Later, from real code
Runtime, after the proving slice. Deployment, after the pipeline.
A sequence diagram of code nobody has written describes a guess.
Who has seen an architecture diagram they could not read without its author in the room?
One diagram per level: Context · Containers · Components · Code
What runs, what stores data, which technology, and the protocol on every arrow.
Components: a table, template section 5.2
One row per use case area. Responsibility, dependencies, status.
Code: never by hand
Your IDE and your agent draw it from the code when you need it.
A diagram made of cloud-provider icons tells you exactly what was bought and nothing about why.
Runtime
Sign-in, a two-hour JWT, then an ownership or membership check on every request
Deployment
One Azure Web App, one container, a staging slot swapped into production
Real endpoints, real token lifetimes, real slots: facts that exist only once the code and the pipeline do.
Architecture is the expensive decisions, chosen by quality attributes, drawn so it reads without you.
Your use case areas.
| Use case area | Package |
|---|---|
WAR, EVA, RUB |
activity, evaluation, rubric |
SEC, TEA, STU, INS |
section, team, student, instructor |
| The RAM areas | five packages under ram/ |
| Cross-cutting | security, system, course |
By domain: Project Pulse
activity/
├── Activity.java
├── ActivityCategory.java
├── ActivityController.java
├── ActivityRepository.java
├── ActivitySecurityService.java
├── ActivityService.java
├── ActivitySpecs.java
├── ActivityStatus.java
├── converter/
└── dto/By layer: the same files, rearranged
controller/ActivityController.java
service/ActivityService.java
service/ActivitySecurityService.java
repository/ActivityRepository.java
repository/ActivitySpecs.java
model/Activity.java
model/ActivityCategory.java
model/ActivityStatus.java
dto/ActivityDto.java
converter/ActivityDtoToActivityConverter.java
converter/ActivityToActivityDtoConverter.javaThe most common change on any project is "change how this one feature works." How many folders do you open in each?
60 to 90% of what software costs is spent changing it after it ships.

package team.projectpulse.activity;
interface ActivityRepository // no "public"
extends JpaRepository<Activity, Integer> { }Package by domain, and the compiler keeps other features out of your repository.
Delete activity/ from Project Pulse. What breaks?
A defect where two layers meet belongs to nobody when the layers belong to different people.
Your template requires exactly one decision by Friday. This is it.
Tmall, Singles' Day, November 11, 2019
544,000 orders per second at peak.
None of it delivers a use case.
Fowler
Almost every successful microservice system started as a monolith that was split. Almost every one built as microservices from day one got into serious trouble.
Prime Video, 2023
Moved a monitoring service from distributed serverless components into one process. Infrastructure cost fell by over 90%.
Which requirement would force several deployables?
Microservices: the shape Project Pulse will need on the day it becomes every capstone course's go-to tool. We expect that any semester now.
| Layer | Its job | Never here |
|---|---|---|
| Controller | Translate HTTP to the application | Business logic |
| Service | Carry out a use case: the business logic, the transaction | HTTP |
| Repository | Load and store data | Business logic |
500 universities. 150,000 students. Every reminder at 9:00 a.m. Monday.
None of the next six slides exists in Project Pulse's code today.
You need it when: Many services, located and replaced dynamically.
In practice: Kubernetes Services, Consul, Eureka.
You need it when: Work that can wait, spikes to absorb, many independent consumers of one event.
In practice: Kafka, Azure Service Bus topics, Spring events.
You need it when: A job longer than a user will wait, or bursts the database cannot absorb.
In practice: RabbitMQ, Amazon SQS, Azure Service Bus queues.
You need it when: Reads far above writes, or an availability target one server cannot meet.
In practice: MySQL replication, Azure MySQL read replicas.
You need it when: Batch computation that can be parallelized.
In practice: Apache Spark, Jenkins agents.
You need it when: You have already chosen microservices.
In practice: Spring Cloud Gateway, Kong, Azure API Management.
If your specification contains none of those needs, your system uses none of those patterns. That is a correct architecture, not an unambitious one.
Between what you control and what you do not.
/api/v1/**/actuator/**.anyRequest().permitAll()The secrets were in a vault and never in git. Every checklist item passed.
It was the absence of a rule.
Deny by default. A new API endpoint fails closed until someone writes its rule.
Session 1: {"error": "Not found"}. Session 2: a bare 500. Session 3: LocalDateTime.now().
| Concept | The rule |
|---|---|
| API shape | /api/v1/<resource>; search is POST /<resource>/search |
| Envelope | Every response is a Result: flag, code, message, data |
| Errors | Services throw; one handler answers for all of them |
| Caller | Who is calling comes from UserUtils, nowhere else |
| Time | Calendar time from the injected Clock |
| Schema | Every change is a new Flyway migration |
| Code | No Lombok, no MapStruct; constructor injection |
| Client | Every browser call goes through one shared HTTP client |
{ "flag": true, "code": 200, "message": "Find activity successfully", "data": { "activityId": 7 } }
{ "flag": false, "code": 404, "message": "Could not find activity with Id 999 :(", "data": null }
{ "flag": false, "code": 500, "message": "A server internal error occurs.", "data": "<the exception's own message>" }Read the last one. What could be in data?
What is logged, at what level, and what must never be?
Project Pulse: Spring Boot defaults, five log statements, no answer.
A convention nothing checks is one you are trusting the agent to remember.
Without a rejected alternative it is a description. Without a trade-off you have not understood it.
KD-database: We use PostgreSQL.
What is missing? Rewrite it in two minutes with your neighbour.
flowchart LR
s1["SEC-authorization"] --> s2["ASR-student-record-<br/>confidentiality"] --> s3["KD-self-issued-jwt +<br/>two-layer auth"] --> s4["route guards,<br/>scoped queries"] --> s5["QS-cross-team-<br/>denial"] --> s6["tests pass"]
m1["MNT-feature-locality"] --> m2["ASR-maintainability-<br/>learnability"] --> m3["KD-vertical-<br/>slices"] --> m4["package<br/>per domain"] --> m5["QS-add-bounded-<br/>context"] -.-> m6["no test:<br/>12 violations"]
style m6 stroke-dasharray: 5 5A chain with no test at the end is a decision you are trusting people to remember.
Microservices, a message queue, Kubernetes, a cache, an API gateway. Fluent, correct, and confident.
For each part: which requirement forces it?

main by 11:59 pmKD-deployment-shape, lastYour agent draws the diagrams. The ranking and the decision are yours.
Decide what is expensive to change. Let quality attributes choose. Cite the requirement, or cut the part.