$npx -y skills add sabahattink/antigravity-fullstack-hq --skill software-architectureSystem design patterns, Clean Architecture, SOLID principles, domain modeling. Use when making architectural decisions, designing new modules, refactoring a tangled codebase, or reviewing system design.
| 1 | # Software Architecture |
| 2 | |
| 3 | ## Clean Architecture Layers |
| 4 | |
| 5 | ``` |
| 6 | ┌─────────────────────────────────────────────┐ |
| 7 | │ Frameworks & Drivers │ ← NestJS, TypeORM, Express |
| 8 | ├─────────────────────────────────────────────┤ |
| 9 | │ Interface Adapters │ ← Controllers, Repositories, Presenters |
| 10 | ├─────────────────────────────────────────────┤ |
| 11 | │ Application Layer │ ← Use Cases, Application Services |
| 12 | ├─────────────────────────────────────────────┤ |
| 13 | │ Domain Layer │ ← Entities, Value Objects, Domain Services |
| 14 | └─────────────────────────────────────────────┘ |
| 15 | |
| 16 | Dependency Rule: outer layers depend on inner layers — NEVER the reverse. |
| 17 | ``` |
| 18 | |
| 19 | ## Domain Layer |
| 20 | |
| 21 | ### Entities |
| 22 | ```typescript |
| 23 | // domain/user/user.entity.ts |
| 24 | // Entities contain identity and business rules. No framework dependencies. |
| 25 | |
| 26 | export class User { |
| 27 | private constructor( |
| 28 | public readonly id: UserId, |
| 29 | public readonly email: Email, |
| 30 | private _name: string, |
| 31 | private _role: UserRole, |
| 32 | public readonly createdAt: Date, |
| 33 | ) {} |
| 34 | |
| 35 | static create(props: { |
| 36 | id: UserId |
| 37 | email: Email |
| 38 | name: string |
| 39 | role?: UserRole |
| 40 | }): User { |
| 41 | if (!props.name.trim()) { |
| 42 | throw new DomainError('Name cannot be empty') |
| 43 | } |
| 44 | return new User( |
| 45 | props.id, |
| 46 | props.email, |
| 47 | props.name.trim(), |
| 48 | props.role ?? UserRole.USER, |
| 49 | new Date(), |
| 50 | ) |
| 51 | } |
| 52 | |
| 53 | get name(): string { return this._name } |
| 54 | get role(): UserRole { return this._role } |
| 55 | |
| 56 | rename(newName: string): User { |
| 57 | if (!newName.trim()) throw new DomainError('Name cannot be empty') |
| 58 | // Return new instance — immutability |
| 59 | return new User(this.id, this.email, newName.trim(), this._role, this.createdAt) |
| 60 | } |
| 61 | |
| 62 | promote(to: UserRole, by: User): User { |
| 63 | if (by.role !== UserRole.ADMIN) { |
| 64 | throw new DomainError('Only admins can promote users') |
| 65 | } |
| 66 | return new User(this.id, this.email, this._name, to, this.createdAt) |
| 67 | } |
| 68 | } |
| 69 | ``` |
| 70 | |
| 71 | ### Value Objects |
| 72 | ```typescript |
| 73 | // domain/shared/value-objects/email.vo.ts |
| 74 | export class Email { |
| 75 | private constructor(public readonly value: string) {} |
| 76 | |
| 77 | static create(raw: string): Email { |
| 78 | const normalized = raw.toLowerCase().trim() |
| 79 | if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) { |
| 80 | throw new DomainError(`Invalid email: ${raw}`) |
| 81 | } |
| 82 | return new Email(normalized) |
| 83 | } |
| 84 | |
| 85 | equals(other: Email): boolean { |
| 86 | return this.value === other.value |
| 87 | } |
| 88 | |
| 89 | toString(): string { |
| 90 | return this.value |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // domain/shared/value-objects/money.vo.ts |
| 95 | export class Money { |
| 96 | private constructor( |
| 97 | public readonly amount: number, // in cents |
| 98 | public readonly currency: string, |
| 99 | ) {} |
| 100 | |
| 101 | static of(amount: number, currency: string): Money { |
| 102 | if (amount < 0) throw new DomainError('Amount cannot be negative') |
| 103 | if (!['USD', 'EUR', 'GBP', 'TRY'].includes(currency)) { |
| 104 | throw new DomainError(`Unsupported currency: ${currency}`) |
| 105 | } |
| 106 | return new Money(Math.round(amount), currency) |
| 107 | } |
| 108 | |
| 109 | add(other: Money): Money { |
| 110 | if (this.currency !== other.currency) { |
| 111 | throw new DomainError('Cannot add different currencies') |
| 112 | } |
| 113 | return Money.of(this.amount + other.amount, this.currency) |
| 114 | } |
| 115 | |
| 116 | format(): string { |
| 117 | return new Intl.NumberFormat('en-US', { |
| 118 | style: 'currency', |
| 119 | currency: this.currency, |
| 120 | }).format(this.amount / 100) |
| 121 | } |
| 122 | } |
| 123 | ``` |
| 124 | |
| 125 | ### Domain Events |
| 126 | ```typescript |
| 127 | // domain/shared/domain-event.ts |
| 128 | export abstract class DomainEvent { |
| 129 | public readonly occurredAt: Date |
| 130 | public readonly eventId: string |
| 131 | |
| 132 | constructor( |
| 133 | public readonly aggregateId: string, |
| 134 | public readonly eventType: string, |
| 135 | ) { |
| 136 | this.occurredAt = new Date() |
| 137 | this.eventId = crypto.randomUUID() |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // domain/user/events/user-created.event.ts |
| 142 | export class UserCreatedEvent extends DomainEvent { |
| 143 | constructor( |
| 144 | public readonly userId: string, |
| 145 | public readonly email: string, |
| 146 | public readonly name: string, |
| 147 | ) { |
| 148 | super(userId, 'user.created') |
| 149 | } |
| 150 | } |
| 151 | ``` |
| 152 | |
| 153 | ## Application Layer |
| 154 | |
| 155 | ### Use Cases (Command/Query pattern) |
| 156 | ```typescript |
| 157 | // application/users/commands/create-user/create-user.command.ts |
| 158 | export class CreateUserCommand { |
| 159 | constructor( |
| 160 | public readonly email: string, |
| 161 | public readonly password: string, |
| 162 | public readonly name: string, |
| 163 | public readonly actorId: string, |
| 164 | ) {} |
| 165 | } |
| 166 | |
| 167 | // application/users/commands/create-user/create-user.handler.ts |
| 168 | import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs' |
| 169 | |
| 170 | @CommandHandler(CreateUserCommand) |
| 171 | export class CreateUserHandler implements ICommandHandler<CreateUserCommand> { |
| 172 | constructor( |
| 173 | private readonly userRepo: IUserRepository, |
| 174 | private readonly hasher: IPasswordHasher, |
| 175 | private readonly eventBus: EventBus |