634 lines
19 KiB
Markdown
634 lines
19 KiB
Markdown
---
|
|
name: software-architecture
|
|
model: haiku
|
|
description: >
|
|
Architectural patterns for building systems: DDD, Event Sourcing, event-driven communication.
|
|
Use when implementing features, reviewing code, planning issues, refining architecture,
|
|
or making design decisions. Ensures alignment with organizational beliefs about
|
|
auditability, domain modeling, and independent evolution.
|
|
user-invocable: false
|
|
---
|
|
|
|
# Software Architecture
|
|
|
|
Architectural patterns and best practices. This skill is auto-triggered when implementing, reviewing, or planning work that involves architectural decisions.
|
|
|
|
## Architecture Beliefs
|
|
|
|
These outcome-focused beliefs (from our organization manifesto) guide architectural decisions:
|
|
|
|
| Belief | Why It Matters |
|
|
|--------|----------------|
|
|
| **Auditability by default** | Systems should remember what happened, not just current state |
|
|
| **Business language in code** | Domain experts' words should appear in the codebase |
|
|
| **Independent evolution** | Parts should change without breaking other parts |
|
|
| **Explicit over implicit** | Intent and side effects should be visible and traceable |
|
|
|
|
## Beliefs → Patterns
|
|
|
|
| Belief | Primary Pattern | Supporting Patterns |
|
|
|--------|-----------------|---------------------|
|
|
| Auditability by default | Event Sourcing | Immutable events, temporal queries |
|
|
| Business language in code | Domain-Driven Design | Ubiquitous language, aggregates, bounded contexts |
|
|
| Independent evolution | Event-driven communication | Bounded contexts, published language |
|
|
| Explicit over implicit | Commands and Events | Domain events, clear intent |
|
|
|
|
## Event Sourcing
|
|
|
|
**Achieves:** Auditability by default
|
|
|
|
Instead of storing current state, store the sequence of events that led to it.
|
|
|
|
**Core concepts:**
|
|
- **Events** are immutable facts about what happened, named in past tense: `OrderPlaced`, `PaymentReceived`
|
|
- **State** is derived by replaying events, not stored directly
|
|
- **Event store** is append-only - history is never modified
|
|
|
|
**Why this matters:**
|
|
- Complete audit trail for free
|
|
- Debug by replaying history
|
|
- Answer "what was the state at time X?"
|
|
- Recover from bugs by fixing logic and replaying
|
|
|
|
**Trade-offs:**
|
|
- More complex than CRUD for simple cases
|
|
- Requires thinking in events, not state
|
|
- Eventually consistent read models
|
|
|
|
## Domain-Driven Design
|
|
|
|
**Achieves:** Business language in code
|
|
|
|
The domain model reflects how the business thinks and talks.
|
|
|
|
**Core concepts:**
|
|
- **Ubiquitous language** - same terms in code, conversations, and documentation
|
|
- **Bounded contexts** - explicit boundaries where terms have consistent meaning
|
|
- **Aggregates** - clusters of objects that change together, with one root entity
|
|
- **Domain events** - capture what happened in business terms
|
|
|
|
**Why this matters:**
|
|
- Domain experts can read and validate the model
|
|
- New team members learn the domain through code
|
|
- Changes in business rules map clearly to code changes
|
|
|
|
**Trade-offs:**
|
|
- Upfront investment in understanding the domain
|
|
- Boundaries may need to shift as understanding grows
|
|
- Overkill for pure technical/infrastructure code
|
|
|
|
## Event-Driven Communication
|
|
|
|
**Achieves:** Independent evolution
|
|
|
|
Services communicate by publishing events, not calling each other directly.
|
|
|
|
**Core concepts:**
|
|
- **Publish events** when something important happens
|
|
- **Subscribe to events** you care about
|
|
- **No direct dependencies** between publisher and subscriber
|
|
- **Eventual consistency** - accept that not everything updates instantly
|
|
|
|
**Why this matters:**
|
|
- Add new services without changing existing ones
|
|
- Services can be deployed independently
|
|
- Natural resilience - if a subscriber is down, events queue
|
|
|
|
**Trade-offs:**
|
|
- Harder to trace request flow
|
|
- Eventual consistency requires different thinking
|
|
- Need infrastructure for reliable event delivery
|
|
|
|
## Commands and Events
|
|
|
|
**Achieves:** Explicit over implicit
|
|
|
|
Distinguish between requests (commands) and facts (events).
|
|
|
|
**Core concepts:**
|
|
- **Commands** express intent: `PlaceOrder`, `CancelSubscription`
|
|
- Commands can be rejected (validation, business rules)
|
|
- **Events** express facts: `OrderPlaced`, `SubscriptionCancelled`
|
|
- Events are immutable - what happened, happened
|
|
|
|
**Why this matters:**
|
|
- Clear separation of "trying to do X" vs "X happened"
|
|
- Commands validate, events just record
|
|
- Enables replay - reprocess events with new logic
|
|
|
|
## When to Diverge
|
|
|
|
These patterns are defaults, not mandates. Diverge intentionally when:
|
|
|
|
- **Simplicity wins** - a simple CRUD endpoint doesn't need event sourcing
|
|
- **Performance requires it** - sometimes synchronous calls are necessary
|
|
- **Team context** - patterns the team doesn't understand cause more harm than good
|
|
- **Prototyping** - validate ideas before investing in full architecture
|
|
|
|
When diverging, document the decision in the project's `vision.md` Architecture section.
|
|
|
|
## Project-Level Architecture
|
|
|
|
Each project documents architectural choices in `vision.md`:
|
|
|
|
```markdown
|
|
## Architecture
|
|
|
|
This project follows organization architecture patterns.
|
|
|
|
### Alignment
|
|
- Event sourcing for [which aggregates/domains]
|
|
- Bounded contexts: [list contexts and their responsibilities]
|
|
- Event-driven communication between [which services]
|
|
|
|
### Intentional Divergences
|
|
| Area | Standard Pattern | What We Do Instead | Why |
|
|
|------|------------------|-------------------|-----|
|
|
```
|
|
|
|
## Go-Specific Best Practices
|
|
|
|
### Package Organization
|
|
|
|
**Good package structure:**
|
|
```
|
|
project/
|
|
├── cmd/ # Application entry points
|
|
│ └── server/
|
|
│ └── main.go
|
|
├── internal/ # Private packages
|
|
│ ├── domain/ # Core business logic
|
|
│ │ ├── user/
|
|
│ │ └── order/
|
|
│ ├── service/ # Application services
|
|
│ ├── repository/ # Data access
|
|
│ └── handler/ # HTTP/gRPC handlers
|
|
├── pkg/ # Public, reusable packages
|
|
└── go.mod
|
|
```
|
|
|
|
**Package naming:**
|
|
- Short, concise, lowercase: `user`, `order`, `auth`
|
|
- Avoid generic names: `util`, `common`, `helpers`, `misc`
|
|
- Name after what it provides, not what it contains
|
|
- One package per concept, not per file
|
|
|
|
**Package cohesion:**
|
|
- A package should have a single, focused responsibility
|
|
- Package internal files can use internal types freely
|
|
- Minimize exported types - export interfaces, hide implementations
|
|
|
|
### Interfaces
|
|
|
|
**Accept interfaces, return structs:**
|
|
```go
|
|
// Good: Accept interface, return concrete type
|
|
func NewUserService(repo UserRepository) *UserService {
|
|
return &UserService{repo: repo}
|
|
}
|
|
|
|
// Bad: Accept and return interface
|
|
func NewUserService(repo UserRepository) UserService {
|
|
return &userService{repo: repo}
|
|
}
|
|
```
|
|
|
|
**Define interfaces at point of use:**
|
|
```go
|
|
// Good: Interface defined where it's used (consumer owns the interface)
|
|
package service
|
|
|
|
type UserRepository interface {
|
|
FindByID(ctx context.Context, id string) (*User, error)
|
|
}
|
|
|
|
// Bad: Interface defined with implementation (producer owns the interface)
|
|
package repository
|
|
|
|
type UserRepository interface {
|
|
FindByID(ctx context.Context, id string) (*User, error)
|
|
}
|
|
```
|
|
|
|
**Keep interfaces small:**
|
|
- Prefer single-method interfaces
|
|
- Large interfaces indicate missing abstraction
|
|
- Compose small interfaces when needed
|
|
|
|
### Error Handling
|
|
|
|
**Wrap errors with context:**
|
|
```go
|
|
// Good: Wrap with context
|
|
if err != nil {
|
|
return fmt.Errorf("fetching user %s: %w", id, err)
|
|
}
|
|
|
|
// Bad: Return bare error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
**Use sentinel errors for expected conditions:**
|
|
```go
|
|
var ErrNotFound = errors.New("not found")
|
|
var ErrConflict = errors.New("conflict")
|
|
|
|
// Check with errors.Is
|
|
if errors.Is(err, ErrNotFound) {
|
|
// handle not found
|
|
}
|
|
```
|
|
|
|
**Error types for rich errors:**
|
|
```go
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
|
}
|
|
|
|
// Check with errors.As
|
|
var valErr *ValidationError
|
|
if errors.As(err, &valErr) {
|
|
// handle validation error
|
|
}
|
|
```
|
|
|
|
### Dependency Injection
|
|
|
|
**Constructor injection:**
|
|
```go
|
|
type UserService struct {
|
|
repo UserRepository
|
|
logger Logger
|
|
}
|
|
|
|
func NewUserService(repo UserRepository, logger Logger) *UserService {
|
|
return &UserService{
|
|
repo: repo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
```
|
|
|
|
**Wire dependencies in main:**
|
|
```go
|
|
func main() {
|
|
// Create dependencies
|
|
db := database.Connect()
|
|
logger := slog.Default()
|
|
|
|
// Wire up services
|
|
userRepo := repository.NewUserRepository(db)
|
|
userService := service.NewUserService(userRepo, logger)
|
|
userHandler := handler.NewUserHandler(userService)
|
|
|
|
// Start server
|
|
http.Handle("/users", userHandler)
|
|
http.ListenAndServe(":8080", nil)
|
|
}
|
|
```
|
|
|
|
**Avoid global state:**
|
|
- No `init()` for service initialization
|
|
- No package-level variables for dependencies
|
|
- Pass context explicitly, don't store in structs
|
|
|
|
### Testing
|
|
|
|
**Table-driven tests:**
|
|
```go
|
|
func TestUserService_Create(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input CreateUserInput
|
|
want *User
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "valid user",
|
|
input: CreateUserInput{Email: "test@example.com"},
|
|
want: &User{Email: "test@example.com"},
|
|
},
|
|
{
|
|
name: "invalid email",
|
|
input: CreateUserInput{Email: "invalid"},
|
|
wantErr: ErrInvalidEmail,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// arrange, act, assert
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
**Test doubles:**
|
|
- Use interfaces for test doubles
|
|
- Prefer hand-written mocks over generated ones for simple cases
|
|
- Use `testify/mock` or `gomock` for complex mocking needs
|
|
|
|
**Test package naming:**
|
|
- `package user_test` for black-box testing (preferred)
|
|
- `package user` for white-box testing when needed
|
|
|
|
## Generic Architecture Patterns
|
|
|
|
### Layered Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Presentation │ HTTP handlers, CLI, gRPC
|
|
├─────────────────────────────────┤
|
|
│ Application │ Use cases, orchestration
|
|
├─────────────────────────────────┤
|
|
│ Domain │ Business logic, entities
|
|
├─────────────────────────────────┤
|
|
│ Infrastructure │ Database, external services
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
**Rules:**
|
|
- Dependencies point downward only
|
|
- Upper layers depend on interfaces, not implementations
|
|
- Domain layer has no external dependencies
|
|
|
|
### SOLID Principles
|
|
|
|
**Single Responsibility (S):**
|
|
- Each module has one reason to change
|
|
- Split code that changes for different reasons
|
|
|
|
**Open/Closed (O):**
|
|
- Open for extension, closed for modification
|
|
- Add new behavior through new types, not changing existing ones
|
|
|
|
**Liskov Substitution (L):**
|
|
- Subtypes must be substitutable for their base types
|
|
- Interfaces should be implementable without surprises
|
|
|
|
**Interface Segregation (I):**
|
|
- Clients shouldn't depend on interfaces they don't use
|
|
- Prefer many small interfaces over few large ones
|
|
|
|
**Dependency Inversion (D):**
|
|
- High-level modules shouldn't depend on low-level modules
|
|
- Both should depend on abstractions
|
|
|
|
### Dependency Direction
|
|
|
|
```
|
|
┌──────────────┐
|
|
│ Domain │
|
|
│ (no deps) │
|
|
└──────────────┘
|
|
▲
|
|
┌────────────┴────────────┐
|
|
│ │
|
|
┌───────┴───────┐ ┌───────┴───────┐
|
|
│ Application │ │Infrastructure │
|
|
│ (uses domain) │ │(implements │
|
|
└───────────────┘ │ domain intf) │
|
|
▲ └───────────────┘
|
|
│
|
|
┌───────┴───────┐
|
|
│ Presentation │
|
|
│(calls app) │
|
|
└───────────────┘
|
|
```
|
|
|
|
**Key insight:** Infrastructure implements domain interfaces, doesn't define them. This inverts the "natural" dependency direction.
|
|
|
|
### Module Boundaries
|
|
|
|
**Signs of good boundaries:**
|
|
- Modules can be understood in isolation
|
|
- Changes are localized within modules
|
|
- Clear, minimal public API
|
|
- Dependencies flow in one direction
|
|
|
|
**Signs of bad boundaries:**
|
|
- Circular dependencies between modules
|
|
- "Shotgun surgery" - small changes require many file edits
|
|
- Modules reach into each other's internals
|
|
- Unclear ownership of concepts
|
|
|
|
## Repository Health Indicators
|
|
|
|
### Positive Indicators
|
|
|
|
| Indicator | What to Look For |
|
|
|-----------|------------------|
|
|
| Clear structure | Obvious package organization, consistent naming |
|
|
| Small interfaces | Most interfaces have 1-3 methods |
|
|
| Explicit dependencies | Constructor injection, no globals |
|
|
| Test coverage | Unit tests for business logic, integration tests for boundaries |
|
|
| Error handling | Wrapped errors, typed errors for expected cases |
|
|
| Documentation | CLAUDE.md accurate, code comments explain "why" |
|
|
|
|
### Warning Signs
|
|
|
|
| Indicator | What to Look For |
|
|
|-----------|------------------|
|
|
| God packages | `utils/`, `common/`, `helpers/` with 20+ files |
|
|
| Circular deps | Package A imports B, B imports A |
|
|
| Deep nesting | 4+ levels of directory nesting |
|
|
| Huge files | Files with 500+ lines |
|
|
| Interface pollution | Interfaces for everything, even single implementations |
|
|
| Global state | Package-level variables, `init()` for setup |
|
|
|
|
### Metrics to Track
|
|
|
|
- **Package fan-out:** How many packages does each package import?
|
|
- **Cyclomatic complexity:** How complex are the functions?
|
|
- **Test coverage:** What percentage of code is tested?
|
|
- **Import depth:** How deep is the import tree?
|
|
|
|
## Review Checklists
|
|
|
|
### Repository Audit Checklist
|
|
|
|
Use this when evaluating overall repository health.
|
|
|
|
**Structure:**
|
|
- [ ] Clear package organization following Go conventions
|
|
- [ ] No circular dependencies between packages
|
|
- [ ] Appropriate use of `internal/` for private packages
|
|
- [ ] `cmd/` for application entry points
|
|
|
|
**Dependencies:**
|
|
- [ ] Dependencies flow inward (toward domain)
|
|
- [ ] Interfaces defined at point of use (not with implementation)
|
|
- [ ] No global state or package-level dependencies
|
|
- [ ] Constructor injection throughout
|
|
|
|
**Code Quality:**
|
|
- [ ] Consistent naming conventions
|
|
- [ ] No "god" packages (utils, common, helpers)
|
|
- [ ] Errors wrapped with context
|
|
- [ ] Small, focused interfaces
|
|
|
|
**Testing:**
|
|
- [ ] Unit tests for domain logic
|
|
- [ ] Integration tests for boundaries (DB, HTTP)
|
|
- [ ] Tests are readable and maintainable
|
|
- [ ] Test coverage for critical paths
|
|
|
|
**Documentation:**
|
|
- [ ] CLAUDE.md is accurate and helpful
|
|
- [ ] vision.md explains the product purpose
|
|
- [ ] Code comments explain "why", not "what"
|
|
|
|
### Issue Refinement Checklist
|
|
|
|
Use this when reviewing issues for architecture impact.
|
|
|
|
**Scope:**
|
|
- [ ] Issue is a vertical slice (user-visible value)
|
|
- [ ] Changes are localized to specific packages
|
|
- [ ] No cross-cutting concerns hidden in implementation
|
|
|
|
**Design:**
|
|
- [ ] Follows existing patterns in the codebase
|
|
- [ ] New abstractions are justified
|
|
- [ ] Interface changes are backward compatible (or breaking change is documented)
|
|
|
|
**Dependencies:**
|
|
- [ ] New dependencies are minimal and justified
|
|
- [ ] No new circular dependencies introduced
|
|
- [ ] Integration points are clearly defined
|
|
|
|
**Testability:**
|
|
- [ ] Acceptance criteria are testable
|
|
- [ ] New code can be unit tested in isolation
|
|
- [ ] Integration test requirements are clear
|
|
|
|
### PR Review Checklist
|
|
|
|
Use this when reviewing pull requests for architecture concerns.
|
|
|
|
**Structure:**
|
|
- [ ] Changes respect existing package boundaries
|
|
- [ ] New packages follow naming conventions
|
|
- [ ] No new circular dependencies
|
|
|
|
**Interfaces:**
|
|
- [ ] Interfaces are defined where used
|
|
- [ ] Interfaces are minimal and focused
|
|
- [ ] Breaking interface changes are justified
|
|
|
|
**Dependencies:**
|
|
- [ ] Dependencies injected via constructors
|
|
- [ ] No new global state
|
|
- [ ] External dependencies properly abstracted
|
|
|
|
**Error Handling:**
|
|
- [ ] Errors wrapped with context
|
|
- [ ] Sentinel errors for expected conditions
|
|
- [ ] Error types for rich error information
|
|
|
|
**Testing:**
|
|
- [ ] New code has appropriate test coverage
|
|
- [ ] Tests are clear and maintainable
|
|
- [ ] Edge cases covered
|
|
|
|
## Anti-Patterns to Flag
|
|
|
|
### God Packages
|
|
|
|
**Problem:** Packages like `utils/`, `common/`, `helpers/` become dumping grounds.
|
|
|
|
**Symptoms:**
|
|
- 20+ files in one package
|
|
- Unrelated functions grouped together
|
|
- Package imported by everything
|
|
|
|
**Fix:** Extract cohesive packages based on what they provide: `validation`, `httputil`, `timeutil`.
|
|
|
|
### Circular Dependencies
|
|
|
|
**Problem:** Package A imports B, and B imports A (directly or transitively).
|
|
|
|
**Symptoms:**
|
|
- Import cycle compile errors
|
|
- Difficulty understanding code flow
|
|
- Changes cascade unexpectedly
|
|
|
|
**Fix:**
|
|
- Extract shared types to a third package
|
|
- Use interfaces to invert dependency
|
|
- Merge packages if truly coupled
|
|
|
|
### Leaky Abstractions
|
|
|
|
**Problem:** Implementation details leak through abstraction boundaries.
|
|
|
|
**Symptoms:**
|
|
- Database types in domain layer
|
|
- HTTP types in service layer
|
|
- Framework types in business logic
|
|
|
|
**Fix:** Define types at each layer, map between them explicitly.
|
|
|
|
### Anemic Domain Model
|
|
|
|
**Problem:** Domain objects are just data containers, logic is elsewhere.
|
|
|
|
**Symptoms:**
|
|
- Domain types have only getters/setters
|
|
- All logic in "service" classes
|
|
- Domain types can be in invalid states
|
|
|
|
**Fix:** Put behavior with data. Domain types should enforce their own invariants.
|
|
|
|
### Shotgun Surgery
|
|
|
|
**Problem:** Small changes require editing many files across packages.
|
|
|
|
**Symptoms:**
|
|
- Feature adds touch 10+ files
|
|
- Similar changes in multiple places
|
|
- Copy-paste between packages
|
|
|
|
**Fix:** Consolidate related code. If things change together, they belong together.
|
|
|
|
### Feature Envy
|
|
|
|
**Problem:** Code in one package is more interested in another package's data.
|
|
|
|
**Symptoms:**
|
|
- Many calls to another package's methods
|
|
- Pulling data just to compute something
|
|
- Logic that belongs elsewhere
|
|
|
|
**Fix:** Move the code to where the data lives, or extract the behavior to a shared place.
|
|
|
|
### Premature Abstraction
|
|
|
|
**Problem:** Creating interfaces and abstractions before they're needed.
|
|
|
|
**Symptoms:**
|
|
- Interfaces with single implementations
|
|
- "Factory" and "Manager" classes everywhere
|
|
- Configuration for things that never change
|
|
|
|
**Fix:** Write concrete code first. Extract abstractions when you have multiple implementations or need to break dependencies.
|
|
|
|
### Deep Hierarchy
|
|
|
|
**Problem:** Excessive layers of abstraction or inheritance.
|
|
|
|
**Symptoms:**
|
|
- 5+ levels of embedding/composition
|
|
- Hard to trace code flow
|
|
- Changes require understanding many layers
|
|
|
|
**Fix:** Prefer composition over inheritance. Flatten hierarchies where possible.
|