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

  1. What a quality attribute is and what a tactic is
  2. Quality attribute scenarios
  3. Availability tactics
  4. Performance tactics
  5. Security tactics
  6. Modifiability tactics
  7. Summary table: attribute → tactics
  8. Common mistakes and tips
  9. Exercises
  10. Conclusion

  1. 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").

  1. 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.

  1. 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."

  1. 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.

  1. 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.

  1. 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.

  1. 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

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved