Software Architecture
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Architecture Orientation
Why software architecture now?
You have already strengthened three layers of engineering judgment:
- Intermediate Java: APIs, ownership, collections, and implementation tradeoffs
- Data Structures and Algorithms: choosing shapes that fit access patterns
- Database Design: modeling data, enforcing relationships, and protecting promises with transactions
Software architecture connects those skills into a larger question:
How should the parts of a software system be organized so the team can change it safely over time?
For a Java engineer, architecture is not just diagrams. It appears in package boundaries, interfaces, dependency direction, persistence choices, deployment shape, and the way business rules are protected from accidental complexity.
In the next lesson, System Design, we will zoom out to multi-service scale, capacity, and distributed concerns. Here, we focus on the architecture of an application or service: the structure that makes good code easier to keep good.

Architecture Is Decisions
Architecture is a set of hard-to-change decisions
A useful mental model:
Design is how you solve a local problem. Architecture is how you constrain many local solutions so the whole system stays coherent.
Architecture decisions often answer questions like:
- Where do business rules live?
- Which modules are allowed to depend on which other modules?
- Is the database model exposed directly to API callers?
- Do we optimize for fast delivery, independent deployment, auditability, latency, or operational simplicity?
- What must be easy to change later?
Good architecture is not about making everything abstract. It is about making the important changes cheap and the dangerous changes obvious.
A practical rule: if a choice affects many files, many teams, data ownership, deployment, or long-term maintainability, treat it as architectural.

Boundaries In Java
Boundaries beat folder names
A common Java codebase has packages like controller, service, repository, and model. That can be fine, but folder names alone do not create architecture.
A real boundary answers:
- What responsibility belongs here?
- What is this part allowed to know?
- Which direction do dependencies flow?
- Can this part be tested without booting the whole app?
Below is a tiny example. The OrderService owns the business rule. It depends on an interface, not directly on a database implementation. This keeps the core rule testable and prevents persistence details from leaking inward.
```java
import java.util.*;
public class Main {
interface OrderRepository {
void save(String orderId, int totalCents);
}
static class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Integer> rows = new HashMap<>();
public void save(String orderId, int totalCents) {
rows.put(orderId, totalCents);
System.out.println("Saved " + orderId + " = " + totalCents + " cents");
}
}
static class OrderService {
private final OrderRepository repository;
OrderService(OrderRepository repository) {
this.repository = repository;
}
void placeOrder(String orderId, int totalCents) {
if (totalCents <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
repository.save(orderId, totalCents);
}
}
public static void main(String[] args) {
OrderRepository repo = new InMemoryOrderRepository();
OrderService service = new OrderService(repo);
service.placeOrder("order-1001", 2599);
}
}
```
Spot The Boundary
A Java team says their order service has a clean architecture because all files are grouped into packages named controller, service, repository, and model.
Which question best tests whether there is a real architectural boundary?
Ports And Adapters
Keep the center boring
Many maintainable systems use some version of this idea:
outside world -> adapters -> application/core -> ports -> adapters -> outside world
The names vary: hexagonal architecture, clean architecture, onion architecture, or simply dependency inversion at application scale.
The principle is consistent:
- The core contains use cases and business rules.
- Interfaces describe what the core needs.
- Adapters implement those interfaces for databases, HTTP, queues, files, or third-party APIs.
For a data engineering flavored example, imagine an ingestion job. The core rule may validate and normalize events. The source could be Kafka today, S3 tomorrow, or a test list in memory. The core should not care.
```java
import java.util.*;
public class Main {
record Event(String id, String type) {}
interface EventSource {
List<Event> readEvents();
}
interface EventSink {
void write(Event event);
}
static class ListEventSource implements EventSource {
public List<Event> readEvents() {
return List.of(
new Event("e1", " click "),
new Event("e2", "PURCHASE")
);
}
}
static class ConsoleEventSink implements EventSink {
public void write(Event event) {
System.out.println("Writing normalized event: " + event);
}
}
static class IngestionUseCase {
private final EventSource source;
private final EventSink sink;
IngestionUseCase(EventSource source, EventSink sink) {
this.source = source;
this.sink = sink;
}
void run() {
for (Event event : source.readEvents()) {
Event normalized = new Event(event.id(), event.type().trim().toLowerCase());
sink.write(normalized);
}
}
}
public static void main(String[] args) {
new IngestionUseCase(new ListEventSource(), new ConsoleEventSink()).run();
}
}
```
Abstraction Check
True or false:
Adding an interface for every class automatically improves architecture.
Quality Attribute Tradeoffs
Architecture is driven by qualities
Architecture discussions become clearer when you name the quality you are optimizing for.
Common quality attributes include:
- Maintainability: can engineers safely change behavior?
- Testability: can important rules be verified quickly and locally?
- Performance: does the design meet latency or throughput needs?
- Reliability: does the system behave correctly under failure?
- Security: are sensitive operations and data protected?
- Observability: can you understand what happened in production?
- Deployability: can changes be released safely?
The hard part: these goals often trade off.
For example, a simple monolith may be easier to test and deploy early. Splitting too soon can add network calls, operational overhead, and duplicated concepts. But if independent team ownership or release cadence becomes the bottleneck, stronger module or service boundaries may become valuable.
Architecture maturity means asking, what problem are we actually buying complexity to solve?

Decision Record Blank
Fill in the blank:
A lightweight architecture decision record usually captures the context, the decision, and the ________ of that decision.
Evolve Deliberately
Architecture should evolve with pressure
A strong engineer does not start every project with maximum complexity. Instead, they watch for pressure:
- Business rules are duplicated in controllers, jobs, and listeners.
- Database entities leak into API contracts everywhere.
- Tests require the full application for simple rules.
- One change regularly touches unrelated modules.
- Nobody knows which package owns a concept.
When those signals appear, refactor toward clearer boundaries.
The small example below shows a common architectural improvement: move a decision from scattered conditional logic into a named policy. This is not about patterns for their own sake. It is about giving an important rule a home.
```java
public class Main {
interface DiscountPolicy {
int discountCents(int subtotalCents);
}
static class LoyaltyDiscountPolicy implements DiscountPolicy {
public int discountCents(int subtotalCents) {
if (subtotalCents >= 10_000) return 1_500;
if (subtotalCents >= 5_000) return 500;
return 0;
}
}
static class CheckoutService {
private final DiscountPolicy discountPolicy;
CheckoutService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
int finalTotal(int subtotalCents) {
return subtotalCents - discountPolicy.discountCents(subtotalCents);
}
}
public static void main(String[] args) {
CheckoutService checkout = new CheckoutService(new LoyaltyDiscountPolicy());
System.out.println("Final total: " + checkout.finalTotal(12_000) + " cents");
System.out.println("Final total: " + checkout.finalTotal(4_000) + " cents");
}
}
```
Architecture Review Checklist
A practical architecture checklist
Use this before building a feature, reviewing a design, or discussing a refactor:
- Core responsibility: what business capability does this module own?
- Dependency direction: what is it allowed to know about?
- Data ownership: which tables, documents, or events are authoritative?
- Change scenario: what future change are we intentionally making easier?
- Quality attribute: what are we optimizing for, and what are we sacrificing?
- Testing strategy: can the important rules be tested without infrastructure?
- Failure behavior: what happens when a dependency is slow, missing, or wrong?
- Decision record: where will future engineers find the reasoning?
The goal is not to predict everything. The goal is to make the system understandable enough that future changes are deliberate instead of accidental.
Next, System Design will take this same decision-making mindset and apply it across services, networks, storage systems, and scale.
