Software Architecture
Architectural styles, patterns, quality attributes, and ADRs — from monolith to microservices.
What is Software Architecture?
Software architecture is the set of fundamental decisions about a system's structure — how it is divided into components, how those components interact, and what constraints govern those interactions.
The architect (or SA) makes these decisions early in the project. They are expensive to change later because the whole system is built on top of them.
Why Does It Matter for Analysts?
A BA/SA who understands architecture:
Monolithic Architecture
All functionality is deployed as a single unit.
[User] → [Web Server]
↓
[Single Application]
├── Auth module
├── Orders module
├── Inventory module
└── Reporting module
↓
[Database]Pros: Simple to develop, test, and deploy initially. One codebase, one deployment.
Cons: As the system grows — harder to scale, deploy, and understand. One bug can crash the whole system.
When to use: Startups, MVPs, small teams, systems with simple domain.
Service-Oriented Architecture (SOA)
Business functionality is split into reusable services communicating through an Enterprise Service Bus (ESB).
Pros: Reusability, enterprise integration across departments.
Cons: ESB becomes a single point of failure and bottleneck. Complex governance.
When to use: Large enterprises integrating legacy systems (banking, insurance, telco).
Microservices Architecture
The system is decomposed into small, independently deployable services, each owning its data.
[API Gateway]
├── [User Service] → [users_db]
├── [Order Service] → [orders_db]
├── [Payment Service] → [payments_db]
└── [Notify Service] → [Redis / Queue]Pros: Independent deployment, technology diversity, fault isolation, scale each service separately.
Cons: Distributed system complexity, network latency, data consistency challenges, operational overhead.
When to use: Large teams, high-load systems, when independent scaling matters.
Event-Driven Architecture (EDA)
Components communicate by publishing and consuming events asynchronously through a message broker (Kafka, RabbitMQ).
[Order Service] → publishes OrderPlaced → [Kafka]
↓
┌───────────────────┤
[Inventory] [Shipping] [Email]Pros: Loose coupling, high throughput, natural audit trail.
Cons: Eventual consistency, harder to trace flows, ordering guarantees complex.
When to use: High-volume data pipelines, real-time systems, audit-heavy domains.
Serverless Architecture
Business logic runs in stateless functions (AWS Lambda, Azure Functions) triggered by events. No servers to manage.
Pros: Zero infrastructure management, pay-per-use pricing, automatic scaling.
Cons: Cold starts, execution time limits, vendor lock-in, hard to test locally.
When to use: Infrequent workloads, glue functions, event processing pipelines.
Comparison Table
| Style | Deployment | Scaling | Complexity | Best For |
|---|---|---|---|---|
| Monolith | Single unit | Vertical | Low | MVP, small teams |
| SOA | Multiple services + ESB | Moderate | High | Enterprise legacy |
| Microservices | Many services | Per service | Very high | Large teams, high load |
| Event-Driven | Event producers/consumers | Very high | High | Async, streaming |
| Serverless | Functions | Automatic | Medium | Sporadic workloads |
Scenario
A fintech startup is launching a digital lending platform. They expect 50,000 registered users in year 1, growing to 500,000 by year 3. The product team asks the SA: "Should we start with microservices?"
What is an Architecture Decision Record (ADR)?
An ADR is a short document that captures an important architectural decision, its context, and its consequences. It creates a shared understanding and serves as institutional memory.
ADR Template:
ADR-001: Start with Modular Monolith, Migrate Later
Status: Accepted
Context:
The team has 4 engineers, a 3-month runway to MVP, and limited DevOps capacity. The domain is complex (loan origination, credit scoring, disbursement, collections) but the initial scope covers only loan origination and basic account management.
Key constraints:
Decision:
We will build a modular monolith structured as domain modules with clear boundaries:
[Lending Platform Monolith] ├── /auth (Authentication & Authorization) ├── /applicants (Customer profiles) ├── /origination (Loan application flow) ├── /scoring (Credit decisions, scoring rules) ├── /disbursement (Payment integration — Stripe) └── /notifications (Email/SMS templates)
Each module has its own service layer and data access layer. Cross-module calls go through service interfaces, never directly to another module's database tables. This enforces boundaries that will become service boundaries when we split.
Consequences:
Positive:
Negative:
Migration Trigger: When any module handles > 10,000 transactions/hour OR team grows beyond 8 engineers, extract that module as a standalone service.
Key Insight for Analysts
The ADR makes the trade-off explicit. The team didn't say "microservices are bad" — they said "microservices are wrong for our current context." When context changes (team grows, load increases), the decision gets revisited with another ADR.
As a BA/SA, you contribute to ADRs by:
Layered (N-Tier) Architecture
The most common pattern. The system is divided into horizontal layers, each with a specific responsibility. Each layer only communicates with the layer directly below it.
┌──────────────────────────┐ │ Presentation Layer │ → UI, REST controllers, GraphQL resolvers ├──────────────────────────┤ │ Business Logic Layer │ → Use cases, domain rules, orchestration ├──────────────────────────┤ │ Data Access Layer │ → Repositories, ORM, SQL queries ├──────────────────────────┤ │ Database Layer │ → PostgreSQL, MongoDB, Redis └──────────────────────────┘
Pros: Easy to understand, widely adopted, clear separation of concerns.
Cons: "Lasagna code" — changes ripple through all layers. Business logic often leaks into other layers.
Hexagonal Architecture (Ports & Adapters)
Also called "Clean Architecture." The business domain is at the center and knows nothing about databases, UI, or external services. External concerns attach through "ports" (interfaces) and "adapters" (implementations).
[REST API Adapter]
↓
[HTTP Port]
↓
[Application Core]
(domain + use cases)
↓
[Database Port]
↓
[PostgreSQL Adapter] [MongoDB Adapter]Key rule: Dependencies point inward. The domain never imports from infrastructure.
Pros: Highly testable (swap adapters), infrastructure is replaceable, domain is isolated.
Cons: More abstractions, steeper learning curve, initial setup overhead.
When to use: Complex domains, long-lived systems, when testability matters.
CQRS — Command Query Responsibility Segregation
Separate the read model (queries) from the write model (commands).
[Client]
├── Command → [Write Model] → [Write DB]
│ (validates, ↓
│ executes) [Event Published]
│ ↓
└── Query → [Read Model] ← [Read DB (denormalized)]
(no validation,
fast reads)Why CQRS?
- Read and write requirements are different: reads are frequent and need to be fast; writes are rare and need to be correct.
- The write model can be normalized for integrity; the read model can be denormalized for performance.
Pros: Optimized read and write paths, can scale them independently.
Cons: Eventual consistency between read and write models, more complex codebase.
Event Sourcing
Instead of storing the current state of a record, store a sequence of events that led to that state.
Instead of: accounts row: { id: 42, balance: 1500 }Store: AccountOpened { account_id: 42, initial: 0 } MoneyDeposited { account_id: 42, amount: 2000 } MoneyWithdrawn { account_id: 42, amount: 500 } → Current balance: 1500 (replayed from events) ```
Pros: Full audit trail, can reconstruct state at any point in time, natural fit with CQRS.
Cons: Querying current state requires event replay (mitigated with snapshots), schema evolution is complex.
Best fit: Financial systems, compliance-heavy domains, audit logs.
Choosing the Right Pattern
| Pattern | Choose when... |
|---|---|
| Layered | Team is small, domain is straightforward |
| Hexagonal | Complex domain, need high test coverage |
| CQRS | Read and write loads are very different |
| Event Sourcing | Full audit trail required, financial domain |
Scenario
A digital lending company has a loan management system where:
The system suffers from read/write contention — when the risk team runs complex reports, loan officers experience slowdowns when approving applications.
Applying CQRS
### Write Side (Command Model)
POST /loans/apply → ApplyForLoanCommand → LoanApplicationService.apply() ├── Validates eligibility rules ├── Calls external credit bureau API ├── Persists to write_db (normalized, ACID) └── Publishes LoanApplicationCreated event
POST /loans/{id}/approve → ApproveLoanCommand → LoanApplicationService.approve() ├── Validates state machine (must be in 'pending') ├── Persists decision to write_db └── Publishes LoanApproved event ```
Write DB schema (normalized): ```sql loan_applications(id, applicant_id, amount, status, created_at) credit_decisions(id, loan_id, score, decision, decided_at) disbursements(id, loan_id, amount, account_iban, disbursed_at) ```
### Read Side (Query Model)
An event consumer listens to all Loan* events and maintains denormalized read models optimized for each use case:
LoanApplicationCreated → update loan_summary_view LoanApproved → update loan_summary_view + ops_dashboard_view LoanDisbursed → update customer_portal_view + audit_log_view
Read DB tables (denormalized, in a separate read replica or Elasticsearch): ```sql -- Customer portal view customer_loan_view(loan_id, applicant_name, amount, status, next_payment_date, remaining_balance)
-- Operations dashboard ops_dashboard(date, total_applications, approved_count, total_disbursed, avg_score)
-- Audit log audit_log(event_type, loan_id, actor, timestamp, details_json) ```
### Result
GET /my-loans → Read Model (fast, no joins)
GET /operations/daily → Read Model (pre-aggregated)
GET /audit/{loan_id} → Read Model (event log)POST /loans/apply → Write Model (transactional, accurate) POST /loans/{id}/approve → Write Model (state machine validation) ```
Trade-offs the SA Documented
| Decision | Reasoning |
|---|---|
| Eventual consistency (< 500ms lag) | Acceptable for read views; real-time accuracy is only needed for writes |
| Separate read database | Eliminates read/write contention — report queries can no longer slow down loan approvals |
| Snapshot every 100 events | Prevents full event replay on every query for high-volume loans |
Key Takeaway
CQRS is not always the answer — it adds complexity. But when read and write workloads have fundamentally different characteristics, it solves real operational pain. As an SA, your job is to make this trade-off explicit and get stakeholder sign-off on the eventual consistency behavior.
What are Non-Functional Requirements (NFRs)?
NFRs describe HOW a system performs its functions, not WHAT it does. They are also called quality attributes or system-ilities.
Missed or vague NFRs are one of the top causes of project failure and production incidents.
Key Quality Attributes
### Performance
How fast the system responds under normal and peak conditions.
Common measures:
BA/SA Task: Define concrete numbers, not "the system should be fast."
Bad: "The system should respond quickly." Good: "95% of API requests must complete in < 200ms under load of 500 concurrent users."
### Scalability
The system's ability to handle increased load by adding resources.
- Vertical scaling: add more CPU/RAM to existing servers
- Horizontal scaling: add more server instances behind a load balancer
Key question: "What happens when we get 10× more users?"
### Availability & Reliability
- 99.9% = ~8.7 hours downtime/year ("three nines")
- 99.99% = ~52 minutes/year ("four nines")
- 99.999% = ~5 minutes/year ("five nines")
### Security
Protection from unauthorized access, data breaches, and attacks.
Key requirements:
### Maintainability
How easy it is to change, fix, or extend the system.
Indicators:
### Portability
Ability to move the system to different environments (cloud providers, OS, databases).
Quality Attribute Scenario Format
A structured way to make NFRs testable:
| Element | Example |
|---|---|
| Stimulus | 1,000 users simultaneously submit loan applications |
| Source | Marketing campaign drives peak traffic |
| Environment | Normal operation, business hours |
| Artifact | Loan Application API |
| Response | System processes all requests |
| Measure | 95th percentile response time < 500ms, error rate < 0.1% |
This format ensures NFRs are verifiable and unambiguous.
Project: Digital Payment Gateway
A bank is building a payment gateway that will process card transactions for 500 merchant clients. The SA is responsible for capturing NFRs before architecture decisions are made.
NFR Elicitation Questions
The SA ran a structured workshop with the following stakeholders:
Key questions asked: 1. "What is the maximum acceptable downtime per year?" 2. "How many transactions per second at peak (e.g., Black Friday)?" 3. "What is the acceptable response time for a payment authorization?" 4. "What data must be retained and for how long?" 5. "What compliance standards must we meet?"
NFR Specification Document
### Performance
NFR-PERF-01: Payment authorization response time Scenario: Cardholder initiates payment at a merchant terminal Measure: 99th percentile response time < 3 seconds Peak load: 5,000 transactions per second Normal load: 800 transactions per second
NFR-PERF-02: Batch reconciliation processing Scenario: End-of-day batch for 500 merchants Measure: All reconciliation files generated within 2 hours of midnight Volume: Up to 2 million transactions per day ```
### Availability & Recovery
NFR-AVAIL-01: System availability Target: 99.99% uptime (< 52 minutes planned downtime/year) Measurement period: rolling 30-day window Exclusion: approved maintenance windows (max 4 hours/quarter, Sundays 02:00-06:00)
NFR-AVAIL-02: Recovery objectives RTO (Recovery Time Objective): < 15 minutes for payment processing RPO (Recovery Point Objective): 0 transactions lost (all committed transactions durable) ```
### Security & Compliance
NFR-SEC-01: PCI DSS Level 1 compliance All cardholder data encrypted at rest (AES-256) All data in transit encrypted (TLS 1.3 minimum) No full PAN stored post-authorization (tokenization required)
NFR-SEC-02: Fraud detection Real-time fraud scoring on every transaction Latency budget for fraud check: < 200ms (included in NFR-PERF-01)
NFR-SEC-03: Audit logging All authorization attempts (successful and failed) logged Retention: 7 years (regulatory requirement) Log integrity: tamper-evident (hash chaining) ```
### Scalability
NFR-SCALE-01: Peak traffic handling System must handle 3× average load without degradation Horizontal auto-scaling must trigger within 60 seconds of load increase
NFR-SCALE-02: Merchant onboarding capacity Support up to 2,000 merchants within 18 months without re-architecture ```
How NFRs Shaped Architecture
| NFR | Architectural Decision |
|---|---|
| 99.99% availability | Active-active multi-region deployment |
| 0 transactions lost | Synchronous write to two data centers before confirmation |
| PCI DSS compliance | Dedicated cardholder data environment (separate network segment) |
| 5,000 TPS peak | Stateless authorization service behind auto-scaling load balancer |
| 7-year log retention | Write-once append-only log storage (S3 + Glacier) |
Key Lesson
NFRs are not an afterthought — they drive fundamental architecture decisions. A 99.9% availability target allows a simple single-region setup. A 99.99% target requires multi-region active-active, which multiplies infrastructure cost and complexity. The SA's job is to surface this trade-off early so business stakeholders can make an informed decision.