Skip to main content

Architecture Overview

This section covers architectural patterns and system design principles that shape how we build scalable, maintainable, and resilient software systems.

What is Software Architecture?

Software architecture represents the fundamental structures of a software system - the high-level organization of components, their relationships, and the principles governing their design and evolution. Architecture decisions have long-lasting impacts on a system's ability to meet functional and non-functional requirements.

Unlike detailed design which focuses on individual components, architecture addresses:

  • System decomposition: How to break down a large system into manageable parts
  • Component interaction: How different parts communicate and coordinate
  • Cross-cutting concerns: Security, performance, scalability, observability
  • Trade-offs: Balancing competing qualities like consistency vs. availability, simplicity vs. flexibility

Architectural Thinking

Good architectural decisions emerge from understanding:

  1. Business requirements: What problem are we solving? What are the constraints?
  2. Quality attributes: Performance, scalability, maintainability, security, availability
  3. Technical context: Team skills, existing systems, deployment infrastructure
  4. Evolution: How will requirements change? What needs to remain flexible?

Architecture is not about choosing "the best pattern" but selecting appropriate patterns for your specific context.

Core Architectural Principles

Separation of Concerns

Divide your system into distinct sections, each addressing a separate concern. This reduces coupling and increases cohesion.

Example: Separate business logic from data access, presentation from domain logic, infrastructure from application code.

// Poor: Mixed concerns
public class UserService {
public void createUser(String name, String email) {
// Validation
if (name == null || name.isEmpty()) throw new IllegalArgumentException();

// Database access
Connection conn = DriverManager.getConnection("jdbc:...");
PreparedStatement stmt = conn.prepareStatement("INSERT INTO users...");

// Email sending
SmtpClient.send(email, "Welcome!", "...");
}
}

// Better: Separated concerns
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
private final UserValidator validator;

public User createUser(CreateUserCommand command) {
validator.validate(command);
User user = repository.save(new User(command.name(), command.email()));
emailService.sendWelcomeEmail(user);
return user;
}
}

Loose Coupling, High Cohesion

Coupling measures how much one component depends on another. Cohesion measures how related the responsibilities within a component are.

  • Aim for loose coupling: Changes in one component shouldn't force changes in others
  • Aim for high cohesion: Everything in a component should relate to its single purpose

Techniques:

  • Depend on interfaces/abstractions, not concrete implementations
  • Use dependency injection
  • Apply the Dependency Inversion Principle
  • Communicate through well-defined contracts (APIs, events)

Abstraction

Hide complex implementation details behind simple interfaces. This reduces cognitive load and makes systems easier to understand and change.

// High-level abstraction
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}

// Multiple implementations hidden behind abstraction
class StripePaymentProcessor implements PaymentProcessor { }
class PayPalPaymentProcessor implements PaymentProcessor { }

The calling code doesn't need to know how payments are processed, just that they can be processed.

Design for Change

Requirements evolve. Technology changes. Teams grow. Design systems that accommodate change:

  • Identify variation points: What's likely to change? Make those areas pluggable
  • Use stable abstractions: Base designs on concepts that change slowly
  • Favor composition over inheritance: Easier to modify behavior at runtime
  • Avoid premature optimization: Don't add complexity for hypothetical future needs

Fail Fast and Safe

Systems should detect errors early and handle failures gracefully:

  • Validation: Check inputs at system boundaries
  • Circuit breakers: Stop cascading failures (see Resilience)
  • Graceful degradation: Provide reduced functionality rather than complete failure
  • Observability: Make failures visible (see Observability)

Architectural Patterns Covered

This section explores several key architectural patterns:

Microservices Architecture

Decompose applications into small, independent services that communicate over networks. Each service owns its data and can be developed, deployed, and scaled independently.

When to use: Large systems with multiple teams, need for independent scaling, polyglot requirements

Trade-offs: Increased operational complexity, distributed system challenges, eventual consistency

Event-Driven Architecture

Build systems that react to events - significant changes in state. Components communicate asynchronously through event streams, enabling loose coupling and scalability.

When to use: Complex workflows, integration across multiple systems, audit requirements, real-time processing

Trade-offs: Eventual consistency, debugging complexity, event schema evolution

Multi-Tenancy

Serve multiple customers (tenants) from a single application instance while keeping their data isolated and providing tenant-specific customization.

When to use: SaaS applications, shared infrastructure with customer isolation needs

Trade-offs: Complexity in data isolation, resource allocation challenges, testing complexity

Cell-Based Architecture

Partition your system into isolated cells that contain all necessary components to serve a subset of users. Limits blast radius of failures.

When to use: Large-scale systems requiring high availability, geographic distribution

Trade-offs: Routing complexity, cross-cell operations, operational overhead

Making Architectural Decisions

Document Decisions with ADRs

Use Architecture Decision Records (ADRs) to capture important decisions, their context, and consequences. See Technical Design for ADR templates.

Structure:

  1. Context: What's the situation and problem?
  2. Decision: What did we decide?
  3. Consequences: What are the trade-offs?

Evaluate Trade-offs

Every architectural decision involves trade-offs. Use structured evaluation:

Example evaluation matrix:

OptionPerformanceScalabilityMaintainabilityCostSecurityTotal
Monolith9479837
Microservices7965734
Modular Monolith8688838

Weight scores based on your priorities. In this example, if cost and maintainability are critical, the modular monolith scores highest.

Start Simple, Evolve as Needed

Resist the urge to build for massive scale from day one. Start with simpler architectures and evolve:

  1. Monolith first: Easier to develop, deploy, and understand
  2. Modular monolith: Add internal boundaries as complexity grows
  3. Extract services: When specific modules need independent scaling or development

Martin Fowler's Monolith First approach: Build a well-structured monolith, then extract services when you understand the domain boundaries and have proven the need for distribution.

Architecture and Quality Attributes

Different patterns excel at different quality attributes:

Quality AttributeRelevant PatternsKey Considerations
ScalabilityMicroservices, Cell-based, Event-drivenStateless services, horizontal scaling, partitioning
PerformanceMonolith, Caching patternsMinimize network calls, optimize databases, async processing
AvailabilityCell-based, Event-drivenRedundancy, circuit breakers, graceful degradation
MaintainabilityModular design, Clean architectureClear boundaries, good documentation, automated testing
SecurityDefense in depth, Zero trustAuthentication, authorization, encryption, input validation
ObservabilityAll patterns need thisStructured logging, metrics, distributed tracing

See Performance and Security for deeper dives.

Common Architectural Anti-Patterns

Big Ball of Mud

A system with no clear structure - everything depends on everything else. Results from:

  • Lack of architectural vision
  • Accumulated technical debt
  • Constant pressure for quick features over good design

Solution: Introduce boundaries gradually. Start by identifying cohesive modules and creating interfaces between them.

Golden Hammer

Using the same pattern for every problem ("we're a microservices shop, everything must be a microservice").

Solution: Evaluate each problem independently. Choose patterns based on requirements, not familiarity.

Distributed Monolith

Microservices that are tightly coupled - you get the complexity of distribution without the benefits of independence.

Symptoms:

  • Must deploy services together
  • Shared databases across services
  • Synchronous, chatty communication

Solution: Properly decompose around business capabilities. Use async communication and event-driven patterns.

Relationship to Other Guidelines

Architecture decisions influence and are influenced by many other practices:

Further Learning

Books:

  • Software Architecture: The Hard Parts by Neal Ford et al. (2021)
  • Fundamentals of Software Architecture by Mark Richards & Neal Ford (2020)
  • Building Microservices by Sam Newman (2021)
  • Designing Data-Intensive Applications by Martin Kleppmann (2017)

Online Resources:

Practice:

  • Review architectural decisions in your codebase - what worked? What didn't?
  • Participate in architecture review meetings
  • Study open-source projects to see different architectural approaches
  • Write ADRs for significant decisions in your projects