Set explicit model preferences to optimize for speed vs capability: - haiku: 11 commands, 2 agents (issue-worker, pr-fixer), 10 skills Fast execution for straightforward tasks - sonnet: 4 commands (groom, improve, plan-issues, review-pr), 1 agent (code-reviewer) Better judgment for analysis and review tasks - opus: 2 commands (arch-refine-issue, arch-review-repo), 1 agent (software-architect) Deep reasoning for architectural analysis Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
19 KiB
name, model, description, user-invocable
| name | model | description | user-invocable |
|---|---|---|---|
| software-architecture | haiku | 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. | 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:
## 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:
// 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:
// 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:
// 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:
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:
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:
type UserService struct {
repo UserRepository
logger Logger
}
func NewUserService(repo UserRepository, logger Logger) *UserService {
return &UserService{
repo: repo,
logger: logger,
}
}
Wire dependencies in main:
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:
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/mockorgomockfor complex mocking needs
Test package naming:
package user_testfor black-box testing (preferred)package userfor 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.