An architecture is judged not only by the functionality it supports, but by how it supports it: how available, fast, secure, and modifiable it is. These properties are called quality attributes and rarely arise by chance. They are achieved through architectural tactics: concrete, cataloged design decisions that influence the system's response to a stimulus. The concept comes from the Software Engineering Institute (SEI) and the book Software Architecture in Practice. In this lesson we will study the most important tactics for availability, performance, security, and modifiability, with concrete examples and a reference table that maps each attribute to its tactics.
Contents
- What a quality attribute is and what a tactic is
- Quality attribute scenarios
- Availability tactics
- Performance tactics
- Security tactics
- Modifiability tactics
- Summary table: attribute → tactics
- Common mistakes and tips
- Exercises
- Conclusion
- What a quality attribute is and what a tactic is
A quality attribute (or "-ility") is a measurable property that indicates how well the system meets the needs of its stakeholders: availability, performance, security, modifiability, scalability, usability, etc.
A tactic is a design decision that affects the response to a specific stimulus related to an attribute. Unlike a pattern (which combines several tactics and structures), a tactic is a design atom: granular and combinable.
- Pattern = a complete recipe (e.g., Circuit Breaker, Cache-Aside).
- Tactic = an ingredient (e.g., "detect failures with a heartbeat," "introduce concurrency").
- Quality attribute scenarios
To design and measure, quality attributes are expressed as scenarios with six parts. This turns a vague requirement ("the system must be fast") into something verifiable.
| Part | Description | Example |
|---|---|---|
| Source | Who generates the stimulus | External user |
| Stimulus | The event that arrives | 1000 requests/sec |
| Artifact | Which part is affected | Catalog API |
| Environment | System conditions | Normal load |
| Response | What the system does | Processes the requests |
| Measure | Success criterion | p95 latency < 200 ms |
A complete example scenario: "Under normal load (1000 req/s), the catalog API responds with a p95 latency below 200 ms." Tactics are chosen to satisfy scenarios like this one.
- Availability tactics
Availability is the system's ability to be operational and accessible when needed. The tactics are grouped into three categories: detecting failures, recovering from them, and preventing them.
Fault detection:
- Ping/Echo and Heartbeat: a component sends periodic signals; their absence indicates it is down.
- Monitoring and timeouts: response time is watched and a failure is assumed when it expires.
- Health check: endpoints that verify the internal state.
Fault recovery:
- Active/passive redundancy: replicas ready to take over the load.
- Retries with backoff: retry idempotent operations with increasing wait times.
- Circuit Breaker: stop calling a failing service to avoid making the situation worse.
- Graceful degradation: offer reduced service instead of failing completely.
Fault prevention:
- Removal of the service from the pool on anomalies.
- Transactions to maintain consistency.
Example of a Circuit Breaker (declarative, with Resilience4j):
@CircuitBreaker(name = "inventory", fallbackMethod = "defaultStock")
public int checkStock(String product) {
return inventoryClient.getStock(product); // remote call
}
// Fallback method: invoked when the circuit is open or the call fails
public int defaultStock(String product, Throwable t) {
return 0; // graceful degradation: assume no stock instead of failing
}Explanation: the @CircuitBreaker wraps the remote call. If the inventory service accumulates failures, the circuit "opens" and stops invoking it, returning the fallbackMethod. This avoids overwhelming a downed service and protects overall availability. The typical scenario measure would be "the system keeps responding even if inventory is down."
- Performance tactics
Performance measures the ability to respond within limits of time (latency) and volume (throughput). The tactics are divided into managing resource demand and managing the resources themselves.
Managing demand:
- Caching: store expensive results to reuse them.
- Rate limiting and event sampling.
- Reduce overhead: fewer layers, less unnecessary serialization.
Managing resources:
- Concurrency / parallelism: process in parallel.
- Increase resources / scaling: more CPU, memory, or nodes.
- Connection/thread pooling: reuse resources that are expensive to create.
- Asynchronous processing: decouple the request from its processing.
Example of a caching tactic (Cache-Aside pattern):
public Product getProduct(String id) {
Product cached = cache.get(id); // 1. check the cache first
if (cached != null) {
return cached; // 2. hit: immediate response
}
Product p = repository.find(id); // 3. miss: go to the DB (slow)
cache.put(id, p, Duration.ofMinutes(10)); // 4. store with expiration (TTL)
return p;
}Step-by-step explanation: (1) we query the cache; (2) on a hit we avoid the database; (3) if not, we resort to the slow source; (4) we store the result with a time to live (TTL) so as not to serve eternally stale data. The cache drastically reduces latency and backend load, in exchange for managing data consistency.
- Security tactics
Security protects the confidentiality, integrity, and availability of data and services. The tactics are grouped into resisting, detecting, and recovering from attacks.
Resisting attacks:
- Authentication: verify identity (tokens, OAuth2, certificates).
- Authorization: control what each identity can do (RBAC, ABAC).
- Encryption: data in transit (TLS) and at rest.
- Input validation: prevent injection and malicious data.
- Least privilege: each component gets only the permissions it needs.
Detecting attacks:
- Auditing and logging of security events.
- Intrusion and anomaly detection.
Recovering from attacks:
- Credential revocation, backups, traceability for forensics.
Example of declarative authorization (Spring Security):
@PreAuthorize("hasRole('ADMIN')") // administrators only
public void deleteUser(String id) {
repository.delete(id);
}
@PreAuthorize("#owner == authentication.name") // the resource owner
public Order viewOrder(String owner, String orderId) {
return repository.find(orderId);
}Explanation: @PreAuthorize evaluates an expression before executing the method. The first case applies role-based access control (RBAC); the second, a finer rule that checks that the caller is the owner of the resource. Combined with input validation and encryption in transit, it forms a defense in depth. Remember that these decisions have compliance implications (for example, data protection) and should be reviewed with professional judgment.
- Modifiability tactics
Modifiability is the ease and cost with which changes are made. It is the attribute most closely tied to the design principles seen earlier. The tactics aim to localize the impact of a change, prevent ripple effects, and defer binding.
Reduce the size of modules:
- Split large modules into cohesive pieces.
Increase cohesion:
- Group related responsibilities (maintain functional cohesion).
Reduce coupling:
- Encapsulate behind interfaces.
- Use intermediaries (facades, adapters, brokers).
- Restrict dependencies (layer and module rules).
- Refactor to eliminate duplication.
Defer the moment of binding:
- External configuration, dependency injection, plugins, feature flags.
Example of deferring binding through external configuration and injection:
public interface PaymentGateway {
void charge(double amount);
}
@Service
public class CheckoutService {
private final PaymentGateway gateway;
// the concrete implementation is decided outside the code, by configuration
public CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
public void complete(double total) { gateway.charge(total); }
}# application.yml: change the payment provider without touching compiled code payments: provider: stripe # just change this value to "paypal"
Explanation: the service depends on the PaymentGateway interface, not on a concrete provider. Which implementation is injected is decided by configuration (application.yml). Switching from Stripe to PayPal requires neither modifying nor recompiling the business logic: the impact of the change is localized, which is precisely the goal of modifiability.
- Summary table: attribute → tactics
| Quality attribute | Main tactics | Example mechanism |
|---|---|---|
| Availability | Detection (heartbeat, health check), recovery (redundancy, retries, circuit breaker, degradation), prevention | Resilience4j, replicas, load balancers |
| Performance | Caching, concurrency, asynchrony, pools, scaling, rate limiting | Redis, threads, queues, autoscaling |
| Security | Authentication, authorization, encryption, validation, least privilege, auditing | OAuth2/TLS, RBAC, WAF, logs |
| Modifiability | Reduce coupling, increase cohesion, encapsulate, defer binding | Interfaces, DI, plugins, feature flags |
| Scalability | Partitioning, statelessness, load balancing | Sharding, stateless services |
| Testability | Dependency injection, interfaces, state control | Mocks, ports and adapters |
Important note: tactics interact and compete. Adding redundancy (availability) can complicate consistency; adding encryption (security) penalizes performance; adding layers of abstraction (modifiability) adds latency. Architecture is about finding the right balance according to the priority scenarios.
Common Mistakes and Tips
- Optimizing attributes that no one has prioritized. Without concrete scenarios, effort is spent where it does not help. Define measurable scenarios before choosing tactics.
- Ignoring the trade-offs. Every tactic has a cost in another attribute. Make it explicit.
- Confusing a tactic with a pattern. The circuit breaker is a pattern that materializes several tactics; don't conflate them conceptually.
- Adding caching without an invalidation strategy. It is the number-one source of stale-data bugs. Always define a TTL or invalidation.
- Treating security as a final layer. It must be designed from the start (security by design) and reviewed professionally, especially in regulated domains.
- Tip: document tactical decisions and their trade-offs in architecture decision records (ADRs).
Exercises
Exercise 1. Write a 6-part quality attribute scenario for the availability of a payments service.
Exercise 2. An endpoint queries a configuration table that changes once a day but is read thousands of times per minute, and it is slow. Which performance tactic would you apply and why?
Exercise 3. State which modifiability tactic allows changing the SMS-sending provider without recompiling the application, and sketch the design.
Solutions
Solution 1. For example:
Source: payments service node. Stimulus: the node stops responding. Artifact: payments service. Environment: normal operation. Response: a redundant node takes over the traffic automatically. Measure: monthly availability stays above 99.95% and failover takes less than 5 seconds.
Solution 2. A caching tactic: the data changes once a day (low volatility) but is read heavily. Caching the result with a TTL of minutes (or invalidation on update) eliminates the vast majority of accesses to the slow source, reducing latency and load. It fits perfectly with the Cache-Aside pattern.
Solution 3. A defer-binding tactic through interface + injection + external configuration:
public interface SmsGateway { void send(String number, String text); }
@Service
public class NotificationService {
private final SmsGateway sms;
public NotificationService(SmsGateway sms) { this.sms = sms; }
public void notify(String number) { sms.send(number, "Notice"); }
}
// The concrete implementation (Twilio, Vonage...) is selected by configuration,
// without touching NotificationService or recompiling the business logic.Conclusion
Architectural tactics are the repertoire of concrete decisions with which an architect pursues quality attributes. We have seen how to express requirements as measurable scenarios and have gone through tactics for availability (detection, recovery, prevention), performance (caching, concurrency, asynchrony), security (resist, detect, recover), and modifiability (reduce coupling, defer binding). The summary table and the warning about trade-offs are the tools you will carry with you. Every technical decision has a cost and a useful life; when those decisions accumulate without being managed, they generate technical debt, the central topic of the next and final lesson of this module.
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
