Loading...
Loading...
Curated guide for implementing design patterns, architecture decisions, and verified design rules in software projects
npx skill4agent add aradotso/design-skills awesome-software-design-patternsSkill by ara.so — Design Skills collection.
// PHP Example
class Database {
private static ?Database $instance = null;
private PDO $connection;
private function __construct() {
$this->connection = new PDO(
$_ENV['DB_DSN'],
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);
}
public static function getInstance(): Database {
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance;
}
public function getConnection(): PDO {
return $this->connection;
}
}
// Usage
$db = Database::getInstance();// Go Example
package notification
type Notification interface {
Send(message string) error
}
type EmailNotification struct {
recipient string
}
func (e *EmailNotification) Send(message string) error {
// Send email implementation
return nil
}
type SMSNotification struct {
phoneNumber string
}
func (s *SMSNotification) Send(message string) error {
// Send SMS implementation
return nil
}
type NotificationFactory struct{}
func (f *NotificationFactory) Create(notifType, target string) Notification {
switch notifType {
case "email":
return &EmailNotification{recipient: target}
case "sms":
return &SMSNotification{phoneNumber: target}
default:
return nil
}
}
// Usage
factory := &NotificationFactory{}
notif := factory.Create("email", "user@example.com")
notif.Send("Hello World")// Go Example
package query
type SQLQuery struct {
table string
columns []string
where string
orderBy string
limit int
}
type QueryBuilder struct {
query SQLQuery
}
func NewQueryBuilder(table string) *QueryBuilder {
return &QueryBuilder{
query: SQLQuery{table: table, columns: []string{"*"}},
}
}
func (b *QueryBuilder) Select(columns ...string) *QueryBuilder {
b.query.columns = columns
return b
}
func (b *QueryBuilder) Where(condition string) *QueryBuilder {
b.query.where = condition
return b
}
func (b *QueryBuilder) OrderBy(field string) *QueryBuilder {
b.query.orderBy = field
return b
}
func (b *QueryBuilder) Limit(limit int) *QueryBuilder {
b.query.limit = limit
return b
}
func (b *QueryBuilder) Build() string {
sql := "SELECT " + strings.Join(b.query.columns, ", ") + " FROM " + b.query.table
if b.query.where != "" {
sql += " WHERE " + b.query.where
}
if b.query.orderBy != "" {
sql += " ORDER BY " + b.query.orderBy
}
if b.query.limit > 0 {
sql += fmt.Sprintf(" LIMIT %d", b.query.limit)
}
return sql
}
// Usage
query := NewQueryBuilder("users").
Select("id", "name", "email").
Where("age > 18").
OrderBy("created_at DESC").
Limit(10).
Build()// PHP Example - Adapting legacy payment gateway to new interface
interface PaymentGateway {
public function processPayment(float $amount, string $currency): bool;
}
class LegacyPaymentService {
public function pay(int $amountInCents, string $currencyCode): array {
// Legacy payment logic
return ['status' => 'success', 'transaction_id' => 'TXN123'];
}
}
class PaymentAdapter implements PaymentGateway {
private LegacyPaymentService $legacyService;
public function __construct(LegacyPaymentService $legacyService) {
$this->legacyService = $legacyService;
}
public function processPayment(float $amount, string $currency): bool {
$amountInCents = (int)($amount * 100);
$result = $this->legacyService->pay($amountInCents, $currency);
return $result['status'] === 'success';
}
}
// Usage
$legacy = new LegacyPaymentService();
$adapter = new PaymentAdapter($legacy);
$success = $adapter->processPayment(99.99, 'USD');// Go Example
package middleware
type Handler interface {
Handle(request string) string
}
type BaseHandler struct{}
func (h *BaseHandler) Handle(request string) string {
return "Handling: " + request
}
type LoggingDecorator struct {
handler Handler
}
func (d *LoggingDecorator) Handle(request string) string {
log.Printf("Before handling: %s", request)
result := d.handler.Handle(request)
log.Printf("After handling: %s", result)
return result
}
type AuthDecorator struct {
handler Handler
}
func (d *AuthDecorator) Handle(request string) string {
if !isAuthenticated(request) {
return "Unauthorized"
}
return d.handler.Handle(request)
}
// Usage - Stack decorators
handler := &BaseHandler{}
handler = &LoggingDecorator{handler: handler}
handler = &AuthDecorator{handler: handler}
result := handler.Handle("user-request")// PHP Example - Different pricing strategies
interface PricingStrategy {
public function calculate(float $basePrice): float;
}
class RegularPricing implements PricingStrategy {
public function calculate(float $basePrice): float {
return $basePrice;
}
}
class SeasonalDiscount implements PricingStrategy {
private float $discountPercent;
public function __construct(float $discountPercent) {
$this->discountPercent = $discountPercent;
}
public function calculate(float $basePrice): float {
return $basePrice * (1 - $this->discountPercent / 100);
}
}
class VIPPricing implements PricingStrategy {
public function calculate(float $basePrice): float {
return $basePrice * 0.80; // 20% off
}
}
class Product {
private float $basePrice;
private PricingStrategy $pricingStrategy;
public function __construct(float $basePrice, PricingStrategy $strategy) {
$this->basePrice = $basePrice;
$this->pricingStrategy = $strategy;
}
public function setStrategy(PricingStrategy $strategy): void {
$this->pricingStrategy = $strategy;
}
public function getPrice(): float {
return $this->pricingStrategy->calculate($this->basePrice);
}
}
// Usage
$product = new Product(100, new RegularPricing());
echo $product->getPrice(); // 100
$product->setStrategy(new SeasonalDiscount(15));
echo $product->getPrice(); // 85
$product->setStrategy(new VIPPricing());
echo $product->getPrice(); // 80// Go Example - Event system
package events
type Event struct {
Name string
Data interface{}
}
type Observer interface {
Update(event Event)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Attach(observer Observer) {
s.observers = append(s.observers, observer)
}
func (s *Subject) Detach(observer Observer) {
for i, obs := range s.observers {
if obs == observer {
s.observers = append(s.observers[:i], s.observers[i+1:]...)
break
}
}
}
func (s *Subject) Notify(event Event) {
for _, observer := range s.observers {
observer.Update(event)
}
}
// Concrete observers
type EmailNotifier struct{}
func (e *EmailNotifier) Update(event Event) {
log.Printf("Email notification for event: %s", event.Name)
}
type LogObserver struct{}
func (l *LogObserver) Update(event Event) {
log.Printf("Logging event: %s with data: %v", event.Name, event.Data)
}
// Usage
subject := &Subject{}
subject.Attach(&EmailNotifier{})
subject.Attach(&LogObserver{})
subject.Notify(Event{
Name: "UserRegistered",
Data: map[string]string{"email": "user@example.com"},
})project/
├── domain/ # Business logic, entities, domain events
├── application/ # Use cases, application services
├── infrastructure/ # External dependencies (DB, API, queue)
└── interfaces/ # Controllers, CLI, API handlers// domain/user.go - Core business entity
package domain
import "errors"
type User struct {
ID string
Email string
Password string
Active bool
}
func NewUser(email, password string) (*User, error) {
if email == "" {
return nil, errors.New("email required")
}
if len(password) < 8 {
return nil, errors.New("password must be at least 8 characters")
}
return &User{
Email: email,
Password: password,
Active: true,
}, nil
}
type UserRepository interface {
Save(user *User) error
FindByEmail(email string) (*User, error)
}
// application/register_user.go - Use case
package application
import "project/domain"
type RegisterUserUseCase struct {
repo domain.UserRepository
}
func NewRegisterUserUseCase(repo domain.UserRepository) *RegisterUserUseCase {
return &RegisterUserUseCase{repo: repo}
}
func (uc *RegisterUserUseCase) Execute(email, password string) error {
// Check if user exists
existing, _ := uc.repo.FindByEmail(email)
if existing != nil {
return errors.New("user already exists")
}
// Create new user
user, err := domain.NewUser(email, password)
if err != nil {
return err
}
// Save user
return uc.repo.Save(user)
}
// infrastructure/postgres_user_repository.go - Implementation
package infrastructure
import (
"database/sql"
"project/domain"
)
type PostgresUserRepository struct {
db *sql.DB
}
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Save(user *domain.User) error {
_, err := r.db.Exec(
"INSERT INTO users (email, password, active) VALUES ($1, $2, $3)",
user.Email, user.Password, user.Active,
)
return err
}
func (r *PostgresUserRepository) FindByEmail(email string) (*domain.User, error) {
user := &domain.User{}
err := r.db.QueryRow(
"SELECT id, email, password, active FROM users WHERE email = $1",
email,
).Scan(&user.ID, &user.Email, &user.Password, &user.Active)
if err == sql.ErrNoRows {
return nil, nil
}
return user, err
}
// interfaces/http/handler.go - API layer
package http
import (
"encoding/json"
"net/http"
"project/application"
)
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UserHandler struct {
registerUseCase *application.RegisterUserUseCase
}
func NewUserHandler(registerUseCase *application.RegisterUserUseCase) *UserHandler {
return &UserHandler{registerUseCase: registerUseCase}
}
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err := h.registerUseCase.Execute(req.Email, req.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}docs/adr/0001-use-postgresql-for-primary-database.md# 1. Use PostgreSQL for Primary Database
Date: 2024-01-15
## Status
Accepted
## Context
We need to choose a primary database for our application that will store user data,
transactions, and business entities. Requirements:
- ACID transactions
- Complex queries with JOINs
- Strong consistency
- Open source with good community support
- Performance for 10M+ records
## Decision
We will use PostgreSQL 15+ as our primary relational database.
## Consequences
### Positive
- Strong ACID guarantees for financial data
- Excellent JSON support for flexible schemas
- Rich ecosystem of tools (PgBouncer, pg_stat_statements, pgAdmin)
- Built-in full-text search capabilities
- Easy horizontal read scaling with replicas
### Negative
- More complex to scale writes compared to NoSQL solutions
- Requires careful index management for query performance
- Backup/restore operations need planning at scale
- Vertical scaling limits eventual
### Neutral
- Team needs to maintain PostgreSQL expertise
- Will use managed service (AWS RDS) to reduce operational burden# Install adr-tools
brew install adr-tools # macOS
apt-get install adr-tools # Ubuntu
# Initialize ADR directory
adr init docs/architecture/decisions
# Create new ADR
adr new "Use event sourcing for order management"
# Link ADRs
adr new -l "2:Amends:partial replacement" "Use CQRS with event sourcing"
# Generate table of contents
adr generate toc > docs/architecture/decisions/README.md# Install log4brains
npm install -g log4brains
# Initialize in project
log4brains init
# Create new ADR interactively
log4brains adr new
# Preview documentation site
log4brains preview
# Build static site
log4brains build.log4brains.ymlproject:
name: "My Project"
tz: "America/New_York"
adr:
defaultStatus: "proposed"
defaultTemplate: "madr"
output:
path: "dist"workspace "E-Commerce System" {
model {
customer = person "Customer" "Places orders online"
ecommerce = softwareSystem "E-Commerce Platform" {
webapp = container "Web Application" "React SPA" "JavaScript"
api = container "API Gateway" "REST API" "Go"
orderService = container "Order Service" "Manages orders" "Go"
paymentService = container "Payment Service" "Processes payments" "Go"
database = container "Database" "Stores data" "PostgreSQL"
messageQueue = container "Message Queue" "Event streaming" "RabbitMQ"
}
paymentGateway = softwareSystem "Payment Gateway" "External payment processor"
# Relationships
customer -> webapp "Uses"
webapp -> api "Makes API calls to" "HTTPS/JSON"
api -> orderService "Routes requests to"
api -> paymentService "Routes requests to"
orderService -> database "Reads/writes"
orderService -> messageQueue "Publishes events"
paymentService -> messageQueue "Subscribes to events"
paymentService -> paymentGateway "Processes payments via"
}
views {
systemContext ecommerce {
include *
autolayout lr
}
container ecommerce {
include *
autolayout lr
}
theme default
}
}# System Architecture
```mermaid
graph TB
Client[Client Application]
API[API Gateway]
Auth[Auth Service]
User[User Service]
Order[Order Service]
DB[(PostgreSQL)]
Cache[(Redis)]
Client -->|HTTPS| API
API -->|Verify Token| Auth
API -->|User Requests| User
API -->|Order Requests| Order
User -->|Read/Write| DB
Order -->|Read/Write| DB
User -->|Cache| Cache
Order -->|Cache| CachesequenceDiagram
participant U as User
participant A as API
participant O as Order Service
participant P as Payment Service
participant Q as Queue
U->>A: POST /orders
A->>O: Create Order
O->>Q: Publish OrderCreated
O-->>A: Order ID
A-->>U: 201 Created
Q->>P: OrderCreated Event
P->>P: Process Payment
P->>Q: Publish PaymentProcessed
### D2 Diagrams
Create `architecture.d2`:
```d2
title: Microservices Architecture {
shape: text
near: top-center
}
users: Users {
shape: person
}
api_gateway: API Gateway {
shape: hexagon
}
services: {
auth: Auth Service {
shape: rectangle
}
orders: Order Service {
shape: rectangle
}
payments: Payment Service {
shape: rectangle
}
}
data: {
postgres: PostgreSQL {
shape: cylinder
}
redis: Redis Cache {
shape: cylinder
}
queue: RabbitMQ {
shape: queue
}
}
users -> api_gateway: HTTPS
api_gateway -> services.auth: gRPC
api_gateway -> services.orders: gRPC
api_gateway -> services.payments: gRPC
services.auth -> data.postgres
services.orders -> data.postgres
services.payments -> data.postgres
services.orders -> data.queue: Publish Events
services.payments -> data.queue: Subscribe Events
services.auth -> data.redis: Cache Sessions# Install D2
curl -fsSL https://d2lang.com/install.sh | sh
# Generate SVG
d2 architecture.d2 architecture.svg
# Watch mode for development
d2 --watch architecture.d2 architecture.svg// src/test/java/com/example/ArchitectureTest.java
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
public class ArchitectureTest {
private final JavaClasses classes = new ClassFileImporter()
.importPackages("com.example");
@Test
public void layerDependenciesAreRespected() {
layeredArchitecture()
.layer("Controllers").definedBy("..controller..")
.layer("Services").definedBy("..service..")
.layer("Repositories").definedBy("..repository..")
.whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
.whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
.whereLayer("Repositories").mayOnlyBeAccessedByLayers("Services")
.check(classes);
}
@Test
public void servicesShouldNotDependOnControllers() {
ArchRule rule = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
rule.check(classes);
}
@Test
public void repositoriesShouldBeInterfaces() {
ArchRule rule = classes()
.that().resideInAPackage("..repository..")
.and().haveSimpleNameEndingWith("Repository")
.should().beInterfaces();
rule.check(classes);
}
}// tests/Architecture/LayersTest.php
use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use Arkitect\Expression\ForClasses\HaveNameMatching;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;
return static function (Config $config): void {
$classSet = ClassSet::fromDir(__DIR__.'/../../src');
$rules = [];
// Controllers should not depend on infrastructure
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new NotDependsOnTheseNamespaces('App\Infrastructure'))
->because('Controllers should not know about infrastructure details');
// Domain should not depend on anything
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new NotDependsOnTheseNamespaces(
'App\Controller',
'App\Infrastructure'
))
->because('Domain must remain isolated');
// Repositories must be interfaces
$rules[] = Rule::allClasses()
->that(new HaveNameMatching('*Repository'))
->should(new BeInterface())
->because('Repositories must be interfaces for DIP');
$config
->add($classSet, ...$rules);
};# Run architecture tests
vendor/bin/phparkitect check
# CI integration
vendor/bin/phparkitect check --config=phparkitect.php --stop-on-failurearch_test.gopackage architecture
import (
"testing"
"github.com/arch-go/arch-go/api"
"github.com/arch-go/arch-go/api/configuration"
)
func TestArchitecture(t *testing.T) {
config := configuration.NewConfig()
// Domain should not depend on infrastructure
config.AddRule(
api.Rule{
Package: "github.com/myorg/myapp/domain",
ShouldNotDependOn: []string{
"github.com/myorg/myapp/infrastructure",
"github.com/myorg/myapp/interfaces",
},
Description: "Domain must be independent",
},
)
// Infrastructure should not depend on interfaces
config.AddRule(
api.Rule{
Package: "github.com/myorg/myapp/infrastructure",
ShouldNotDependOn: []string{
"github.com/myorg/myapp/interfaces",
},
Description: "Infrastructure should not know about interfaces",
},
)
// Interfaces can depend on application and domain
config.AddRule(
api.Rule{
Package: "github.com/myorg/myapp/interfaces",
ShouldOnlyDependOn: []string{
"github.com/myorg/myapp/application",
"github.com/myorg/myapp/domain",
},
Description: "Interfaces should only depend on application and domain",
},
)
result := api.CheckArchitecture(config)
if !result.Pass {
t.Fatalf("Architecture rules violated:\n%s", result.String())
}
}.dependency-cruiser.jsmodule.exports = {
forbidden: [
{
name: 'no-circular',
severity: 'error',
from: {},
to: { circular: true }
},
{
name: 'no-orphans',
severity: 'warn',
from: { orphan: true, pathNot: '\\.(test|spec)\\.' },
to: {}
},
{
name: 'domain-isolation',
severity: 'error',
from: { path: '^src/domain' },
to: {
path: '^src/(infrastructure|interfaces)',
pathNot: '^src/domain'
}
},
{
name: 'no-infrastructure-in-application',
severity: 'error',
from: { path: '^src/application' },
to: { path: '^src/infrastructure' }
}
],
options: {
doNotFollow: {
path: 'node_modules'
},
tsConfig: {
fileName: 'tsconfig.json'
}
}
};# Install
npm install --save-dev dependency-cruiser
# Validate architecture
npx depcruise --validate .dependency-cruiser.js src
# Generate graph
npx depcruise --include-only "^src" --output-type dot src | dot -T svg > architecture-graph.svg# .github/workflows/architecture-checks.yml
name: Architecture Verification
on: [push, pull_request]
jobs:
architecture-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run Architecture Tests
run: go test ./tests/architecture/...
- name: Check Module Dependencies
run: |
go install github.com/arch-go/arch-go@latest
go test -v ./arch_test.go// domain/events.go
package domain
import "time"
type Event interface {
EventType() string
OccurredAt() time.Time
}
type OrderCreated struct {
OrderID string
CustomerID string
Amount float64
Timestamp time.Time
}
func (e OrderCreated) EventType() string { return "OrderCreated" }
func (e OrderCreated) OccurredAt() time.Time { return e.Timestamp }
type OrderPaid struct {
OrderID string
PaymentID string
Amount float64
Timestamp time.Time
}
func (e OrderPaid) EventType() string { return "OrderPaid" }
func (e OrderPaid) OccurredAt() time.Time { return e.Timestamp }
// Aggregate
type Order struct {
ID string
CustomerID string
Amount float64
Status string
events []Event
}
func (o *Order) Apply(event Event) {
switch e := event.(type) {
case OrderCreated:
o.ID = e.OrderID
o.CustomerID = e.CustomerID
o.Amount = e.Amount
o.Status = "pending"
case OrderPaid:
o.Status = "paid"
}
o.events = append(o.events, event)
}
func (o *Order) GetUncommittedEvents() []Event {
return o.events
}
// Event Store
type EventStore interface {
SaveEvents(aggregateID string, events []Event) error
GetEvents(aggregateID string) ([]Event, error)
}<?php
// Command side
namespace App\Application\Command;
class CreateOrderCommand
{
public function __construct(
public readonly string $orderId,
public readonly string $customerId,
public readonly float $amount
) {}
}
class CreateOrderHandler
{
public function __construct(
private OrderRepository $repository,
private EventBus $eventBus
) {}
public function handle(CreateOrderCommand $command): void
{
$order = new Order(
$command->orderId,
$command->customerId,
$command->amount
);
$this->repository->save($order);
$this->eventBus->publish(new OrderCreated