
Architecture Patterns
- 2 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
architecture-patterns is a Claude Code skill that documents AI-friendly architecture patterns (DDD, Clean, Hexagonal, Page Object Model) for TypeScript projects.
About
architecture-patterns documents software architecture patterns (Domain-Driven Design, Clean Architecture, Hexagonal Architecture and the Page Object Model) for TypeScript projects. It explains when to use each and how clear layering helps AI generate coherent, maintainable code. A developer uses it to choose and apply an architecture before or during a full-stack build.
- Guides DDD, Clean, Hexagonal and Page Object Model patterns for TypeScript
- Structures projects so AI generates more consistent, layered code
- Ships detailed per-pattern reference guides with TypeScript examples
Architecture Patterns by the numbers
- 2 all-time installs (skills.sh)
- Ranked #3,774 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
architecture-patterns capabilities & compatibility
- Capabilities
- software architecture · domain driven design · clean architecture · hexagonal architecture
- Use cases
- refactoring · api development
What architecture-patterns says it does
AI-friendly architecture patterns for TypeScript projects including Domain-Driven Design, Clean Architecture, Hexagonal Architecture, and Page Object Model testing patterns
Clean Architecture organizes code into concentric layers with strict dependency inversion.
npx skills add https://github.com/aiskillstore/marketplace --skill architecture-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Choose and apply a software architecture pattern (DDD, Clean, Hexagonal, Page Object Model) to structure a full-stack TypeScript project.
Who is it for?
Structuring full-stack TypeScript projects with a clear architecture so AI-generated code stays consistent.
Skip if: Language-specific implementation details outside TypeScript or non-architectural refactors.
When should I use this skill?
A developer needs to pick or apply a software architecture pattern for a TypeScript codebase.
What you get
A chosen architecture pattern with clear layers and interfaces guiding coherent code generation.
- architecture pattern selection
- per-pattern TypeScript guidance
By the numbers
- 4 architecture patterns covered (DDD, Clean, Hexagonal, Page Object Model)
- 5 shared key principles
Files
Architecture Patterns for AI-Assisted Development
Overview
Modern software architecture patterns are extraordinarily effective when working with AI code generation. This skill provides comprehensive guidance on architecture patterns that help AI agents generate correct, coherent, and maintainable code for full-stack TypeScript projects.
Why Architecture Matters for AI
Certain software architecture and design patterns greatly assist AI code-generation tools in producing high-quality output. By structuring projects with clear domain models, layered boundaries, and well-defined patterns, you reduce ambiguity for AI assistants and dramatically increase the consistency and correctness of their output.
Key principles:
- Explicit modeling reduces ambiguity about where logic should go
- Clear separation of concerns helps AI focus on one layer at a time
- Consistent patterns allow AI to reliably replicate your architectural decisions
- Well-defined interfaces act as contracts that guide AI implementation
- Testability becomes easier to achieve when architecture is sound
Available Patterns
Domain-Driven Design (DDD)
DDD focuses on a clear domain model using Entities, Value Objects, Domain Services, and Repositories—all expressed in the ubiquitous language of the business. This explicit modeling gives AI agents a well-defined vocabulary and structure to follow.
Best for:
- Complex business domains with evolving rules
- Systems where business language matters
- Long-lived applications requiring flexibility
- Teams with domain expertise to capture
Key benefit: AI naturally follows domain terminology and structure, producing cohesive code that business stakeholders understand.
Clean Architecture (Layered & Onion)
Clean Architecture organizes code into concentric layers with strict dependency inversion. This pattern provides a clear recipe for where any piece of logic belongs, giving AI a strong guardrail for consistent implementation.
Best for:
- Any TypeScript backend or full-stack application
- Teams wanting clear separation of concerns
- Projects that need to remain testable and maintainable
- Systems likely to need infrastructure changes
Key benefit: AI can implement one layer at a time, with clear responsibilities and interfaces for each layer.
Hexagonal Architecture (Ports & Adapters)
Hexagonal Architecture isolates core business logic from external integrations through explicit Port interfaces and Adapter implementations. The core depends only on abstractions, not on external technology details.
Best for:
- Applications with multiple external integrations (APIs, databases, message queues)
- Systems where switching implementations is important
- Complex systems where testing core logic matters
- Microservices architecture
Key benefit: AI can safely generate testable core logic separately from imperative adapters, with clear integration points.
Page Object Model (POM) for Testing
POM represents each page or significant UI component as a class that encapsulates interactions and locators. Tests use these page objects instead of raw browser commands.
Best for:
- End-to-end testing with Playwright, Cypress, or Selenium
- Web automation scripts
- UI testing that needs to remain maintainable
- Projects using AI to generate test code
Key benefit: AI can generate clear, reusable page objects and coherent test scenarios that are easy to understand and maintain.
How to Use This Skill
1. Choose your pattern based on your project type and complexity 2. Review the detailed guide for that pattern with TypeScript examples 3. Share the architecture with your AI assistant as context for code generation 4. Guide AI implementation layer-by-layer or component-by-component 5. Maintain consistency by referencing the pattern in every prompt
Real-World Results
Teams implementing these patterns with AI code generation have reported:
- 3x faster feature delivery without sacrificing code quality
- 90%+ pattern compliance when using clear architectural guidance
- Significantly fewer refactors due to better initial structure
- Improved testability making code easier to review and modify
- Clear integration points reducing architectural drift
Key Principles Across All Patterns
1. Separation of Concerns
Each layer/component has a single, well-defined responsibility. AI generates more correct code when responsibility is clear.
2. Dependency Inversion
Inner layers don't depend on outer layers. This prevents the AI from generating tightly coupled code.
3. Interface Contracts
Well-defined interfaces between components act as contracts. AI can implement both sides correctly because the contract is explicit.
4. Consistency
Patterns are repetitive. When AI sees one repository interface, it naturally creates similar ones elsewhere.
5. Testability
Good architecture is inherently testable. AI-generated tests are more reliable when the code follows good patterns.
Comparison at a Glance
| Pattern | Best For | Core Concept | AI Benefit |
|---|---|---|---|
| DDD | Complex business domains | Rich domain model in ubiquitous language | Clear vocabulary and structure |
| Clean Architecture | General full-stack apps | Layered with dependency inversion | Clear responsibility per layer |
| Hexagonal | Multi-integration systems | Core logic isolated via ports/adapters | Separate core from imperative code |
| Page Object Model | E2E testing | Page classes encapsulate UI interactions | Reusable, readable test code |
Recommended Combinations
Full-Stack Web Application
DDD + Clean Architecture + Page Object Model
- DDD defines your domain model and use cases
- Clean Architecture structures backend with proper layering
- POM provides maintainable E2E tests
Microservice with Multiple Integrations
DDD + Hexagonal Architecture
- DDD defines bounded context
- Hexagonal isolates core from external services
Testing-Heavy Project
Clean Architecture + Page Object Model
- Clean Architecture ensures testable code
- POM makes test generation reliable
Legacy System Modernization
Start with Hexagonal, then introduce DDD
- Hexagonal helps gradually extract core logic
- DDD helps establish domain vocabulary
Next Steps
1. Read the pattern guide most relevant to your project 2. Review the TypeScript examples provided 3. Show the architecture template to your AI assistant 4. Guide generation step-by-step within each layer/component 5. Enforce patterns through code review (human or automated)
Related Resources
- See
ddd.mdfor Domain-Driven Design deep-dive - See
clean-architecture.mdfor layered architecture examples - See
hexagonal-architecture.mdfor ports and adapters - See
page-object-model.mdfor E2E testing patterns
---
Remember: The goal isn't to follow patterns dogmatically, but to use them as guardrails that help both humans and AI write better code together.
Clean Architecture (Layered & Onion Architecture)
What is Clean Architecture?
Clean Architecture, popularized by Robert C. Martin, organizes code into concentric layers with strict separation of concerns. The rule is that inner layers know nothing of outer layers (dependency inversion principle). This pattern is excellent for AI-generated code because it provides a clear recipe for structuring any feature.
Core Principles
1. Dependency Inversion
Inner layers (domain) never depend on outer layers (infrastructure). Dependencies always point inward.
┌─────────────────────────────────────────────┐
│ Interfaces & Controllers (outermost) │
├─────────────────────────────────────────────┤
│ Interface Adapters (Presenters, Gateways) │
├─────────────────────────────────────────────┤
│ Application Services & Use Cases │
├─────────────────────────────────────────────┤
│ Domain Entities & Business Rules (core) │
└─────────────────────────────────────────────┘
↑ Dependencies flow inward only ↑2. Each Layer Has One Responsibility
- Domain Layer: Pure business rules (no external dependencies)
- Application Layer: Use cases and orchestration
- Interface Adapters: Controllers, presenters, gateways
- Infrastructure Layer: Database, web frameworks, external APIs
3. Testability
Each layer can be tested independently by replacing outer dependencies with test doubles.
4. Flexibility
You can swap infrastructure implementations (swap PostgreSQL for MongoDB) without touching domain logic.
Why Clean Architecture Helps AI Agents
This pattern is excellent for AI-generated code because it provides a clear recipe for structuring any feature. Each piece of logic has a designated place:
- "Business validation goes in the domain or use-case layer, not in the controller"
- "Database access only occurs in repository implementations in the infrastructure layer"
- "Controllers convert HTTP requests to use-case calls"
Engineers using AI have found that Clean/Onion architecture gave a "solid foundation that AI could easily understand and maintain," thanks to its clear separation and dependency rules. The AI doesn't have to guess where code should go; the project structure itself guides it.
How Clean Architecture Improves AI Output
When an AI is instructed to follow Clean Architecture, the likelihood of correct output increases because the generation task is broken down. You can prompt the AI to implement one layer at a time:
- "Write the domain service for X"
- "Implement the repository adapter for Y"
- "Create the controller that uses this use case"
And it will adhere to the boundaries. This compartmentalization means fewer errors in integrating pieces, since each piece conforms to a known interface or contract.
The Four Layers
Layer 1: Domain Layer (Core)
Plain TypeScript classes with business logic and definitions. No external dependencies.
/**
* Domain Entity
* Encapsulates business logic and rules
*/
class Order {
private _id: string;
private _items: OrderItem[] = [];
private _status: OrderStatus;
private _customerId: string;
constructor(customerId: string) {
this._id = generateId();
this._customerId = customerId;
this._status = OrderStatus.PENDING;
}
/**
* Business rule: add line item with validation
*/
addLineItem(productId: string, quantity: number, price: Money): void {
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}
if (this._status !== OrderStatus.PENDING) {
throw new Error('Cannot add items to a confirmed order');
}
const item = new OrderItem(productId, quantity, price);
this._items.push(item);
}
/**
* Business rule: calculate total
*/
calculateTotal(): Money {
return this._items.reduce(
(total, item) => total.add(item.getSubtotal()),
Money.zero()
);
}
confirm(): void {
if (this._items.length === 0) {
throw new Error('Cannot confirm empty order');
}
this._status = OrderStatus.CONFIRMED;
}
get id(): string {
return this._id;
}
get status(): OrderStatus {
return this._status;
}
get items(): OrderItem[] {
return [...this._items]; // defensive copy
}
}Layer 2: Application Layer
Use cases that orchestrate domain operations. Define ports (interfaces) for external interactions.
/**
* Repository Port (interface)
* Defined in application layer, implemented in infrastructure
*/
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
/**
* Use Case Service
* Orchestrates domain objects to fulfill business requirement
*/
class ConfirmOrderUseCase {
constructor(
private orderRepository: OrderRepository,
private paymentService: PaymentServicePort,
private notificationService: NotificationServicePort
) {}
async execute(orderId: string, paymentInfo: PaymentInfo): Promise<OrderDTO> {
// Retrieve aggregate
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
// Apply business rule
order.confirm();
// Delegate to external services via ports
const paymentResult = await this.paymentService.processPayment(
paymentInfo,
order.calculateTotal()
);
if (!paymentResult.success) {
throw new PaymentFailedError(paymentResult.reason);
}
// Persist
await this.orderRepository.save(order);
// Notify
await this.notificationService.sendConfirmation(order);
// Return DTO (not domain object)
return new OrderDTO(order);
}
}Layer 3: Interface Adapters
Controllers and presenters that translate between external world and use cases.
/**
* Controller
* Converts HTTP request to use case input
* Converts use case output to HTTP response
*/
class OrderController {
constructor(private confirmOrderUseCase: ConfirmOrderUseCase) {}
async confirmOrder(
req: Request,
res: Response
): Promise<void> {
try {
const { orderId } = req.params;
const { cardToken, expiryDate } = req.body;
// Validate input
if (!orderId || !cardToken) {
res.status(400).json({
error: 'Missing required fields: orderId, cardToken'
});
return;
}
// Create DTO for use case
const paymentInfo = new PaymentInfo(cardToken, expiryDate);
// Execute use case
const orderDTO = await this.confirmOrderUseCase.execute(
orderId,
paymentInfo
);
// Return response
res.status(200).json(orderDTO);
} catch (error) {
this.handleError(error, res);
}
}
private handleError(error: Error, res: Response): void {
if (error instanceof OrderNotFoundError) {
res.status(404).json({ error: 'Order not found' });
} else if (error instanceof PaymentFailedError) {
res.status(402).json({ error: 'Payment failed' });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
}Layer 4: Infrastructure Layer
Database implementations, HTTP clients, external service integrations.
/**
* Repository Implementation
* Implements the port defined in application layer
*/
class MongoOrderRepository implements OrderRepository {
constructor(private db: MongoClient) {}
async save(order: Order): Promise<void> {
const document = {
_id: order.id,
customerId: order.customerId,
status: order.status,
items: order.items.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.price.value
})),
total: order.calculateTotal().value,
createdAt: new Date()
};
await this.db.collection('orders').insertOne(document);
}
async findById(id: string): Promise<Order | null> {
const document = await this.db.collection('orders').findOne({ _id: id });
if (!document) {
return null;
}
// Reconstruct domain object from persistence
const order = new Order(document.customerId);
document.items.forEach(item => {
order.addLineItem(
item.productId,
item.quantity,
Money.fromValue(item.price)
);
});
return order;
}
}
/**
* Payment Service Adapter
* Implements payment port using external API
*/
class StripePaymentAdapter implements PaymentServicePort {
constructor(private stripeClient: StripeClient) {}
async processPayment(
paymentInfo: PaymentInfo,
amount: Money
): Promise<PaymentResult> {
try {
const charge = await this.stripeClient.charges.create({
amount: amount.cents(),
currency: 'usd',
source: paymentInfo.cardToken
});
return {
success: true,
transactionId: charge.id
};
} catch (error) {
return {
success: false,
reason: error.message
};
}
}
}File Structure for Clean Architecture
src/
├── domain/
│ ├── entities/
│ │ ├── Order.ts
│ │ ├── OrderItem.ts
│ │ └── Customer.ts
│ ├── value-objects/
│ │ ├── Money.ts
│ │ └── OrderStatus.ts
│ ├── services/
│ │ └── OrderCalculationService.ts
│ └── errors/
│ ├── DomainError.ts
│ └── ValidationError.ts
├── application/
│ ├── use-cases/
│ │ ├── ConfirmOrderUseCase.ts
│ │ ├── CancelOrderUseCase.ts
│ │ └── GetOrderDetailsUseCase.ts
│ ├── ports/
│ │ ├── OrderRepository.ts
│ │ ├── PaymentServicePort.ts
│ │ └── NotificationServicePort.ts
│ ├── dto/
│ │ ├── OrderDTO.ts
│ │ └── PaymentInfo.ts
│ └── errors/
│ ├── ApplicationError.ts
│ ├── OrderNotFoundError.ts
│ └── PaymentFailedError.ts
├── interfaces/
│ ├── http/
│ │ ├── OrderController.ts
│ │ ├── routes.ts
│ │ └── middleware/
│ │ └── errorHandler.ts
│ └── cli/
│ └── OrderCLI.ts
├── infrastructure/
│ ├── repositories/
│ │ ├── MongoOrderRepository.ts
│ │ └── PostgresOrderRepository.ts
│ ├── adapters/
│ │ ├── StripePaymentAdapter.ts
│ │ ├── SendGridNotificationAdapter.ts
│ │ └── TwilioSmsAdapter.ts
│ ├── database/
│ │ ├── mongoClient.ts
│ │ └── migrations/
│ └── config/
│ └── dependencies.ts
└── main.tsDependency Injection Setup
/**
* Wire up dependencies
* Typically done in a composition root at application startup
*/
class ApplicationContainer {
private mongoClient: MongoClient;
private orderRepository: OrderRepository;
private paymentService: PaymentServicePort;
private notificationService: NotificationServicePort;
constructor(config: AppConfig) {
// Infrastructure
this.mongoClient = new MongoClient(config.mongoUrl);
this.paymentService = new StripePaymentAdapter(
new StripeClient(config.stripeKey)
);
this.notificationService = new SendGridAdapter(
new SendGridClient(config.sendGridKey)
);
// Repositories
this.orderRepository = new MongoOrderRepository(this.mongoClient);
}
// Factory methods for use cases
getConfirmOrderUseCase(): ConfirmOrderUseCase {
return new ConfirmOrderUseCase(
this.orderRepository,
this.paymentService,
this.notificationService
);
}
getOrderController(): OrderController {
return new OrderController(this.getConfirmOrderUseCase());
}
}Clean Architecture vs. Simpler Approaches
❌ Problematic: Mixed Concerns
// Everything in one file - hard to test, extend, or maintain
app.post('/order/confirm', async (req, res) => {
try {
const order = await db.query(
'SELECT * FROM orders WHERE id = ?',
[req.params.orderId]
);
// Validation mixed with database calls
if (!order || order.rows.length === 0) {
return res.status(404).json({ error: 'Not found' });
}
// Business logic mixed with HTTP and external calls
const total = order.rows[0].items.reduce((sum, item) => sum + item.price, 0);
const chargeResult = await stripe.charges.create({
amount: total * 100,
currency: 'usd',
source: req.body.cardToken
});
// Updates without validation
await db.query('UPDATE orders SET status = ? WHERE id = ?', [
'CONFIRMED',
order.rows[0].id
]);
// Notification mixed in
await sendgrid.send({
to: order.rows[0].customer_email,
subject: 'Order confirmed'
});
res.json(order.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});✅ Clean: Separated Concerns
// Domain: Pure business logic
class Order {
confirm(): void {
if (this._items.length === 0) {
throw new Error('Cannot confirm empty order');
}
this._status = OrderStatus.CONFIRMED;
}
}
// Application: Orchestration
class ConfirmOrderUseCase {
async execute(orderId: string, paymentInfo: PaymentInfo): Promise<void> {
const order = await this.orderRepository.findById(orderId);
order.confirm(); // Business rule
await this.paymentService.process(paymentInfo, order.total);
await this.orderRepository.save(order);
}
}
// Interface: HTTP conversion
class OrderController {
async confirmOrder(req: Request, res: Response): Promise<void> {
const result = await this.confirmOrderUseCase.execute(
req.params.orderId,
req.body.paymentInfo
);
res.json(result);
}
}Tips for AI-Generated Clean Architecture Code
1. Implement one layer at a time - Ask the AI to create domain entities first, then use cases, then adapters 2. Start with interfaces - Define repository and service ports before implementing them 3. Use dependency injection - Show the AI an example of how dependencies are wired 4. Reference existing patterns - Point to similar use cases or repositories as examples 5. Test each layer independently - Request unit tests that mock external dependencies
Common Mistakes to Avoid
Mistake: Business Logic in Controllers
// ❌ Wrong
app.post('/order/confirm', async (req, res) => {
if (req.body.total <= 0) { // Business logic in controller!
res.status(400).send('Invalid total');
}
// ...
});
// ✅ Right
// Business logic in domain or use case
class Order {
confirm(): void {
if (this._total.value <= 0) {
throw new ValidationError('Invalid total');
}
// ...
}
}Mistake: Domain Depending on Infrastructure
// ❌ Wrong
class Order {
async save(): Promise<void> {
// Domain shouldn't know about database!
await db.collection('orders').insertOne(this.toJSON());
}
}
// ✅ Right
// Domain is pure, repository handles persistence
class Order {
// Domain has no side effects
confirm(): void {
this._status = OrderStatus.CONFIRMED;
}
}
// Infrastructure handles persistence
class MongoOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
await this.db.collection('orders').insertOne(order.toJSON());
}
}Mistake: Using Domain Objects as DTOs
// ❌ Wrong - exposes domain details
app.get('/order/:id', async (req, res) => {
const order = await orderRepository.findById(req.params.id);
res.json(order); // Exposes internal state
});
// ✅ Right - use DTOs
app.get('/order/:id', async (req, res) => {
const order = await orderRepository.findById(req.params.id);
res.json(new OrderDTO(order)); // Only expose what clients need
});Key Takeaways
- Clear layers make it obvious where code should go
- Dependency inversion keeps domain pure and testable
- Ports provide clear contracts between layers
- One layer at a time AI implementation reduces errors
- Flexibility to swap implementations without touching domain
- Testability improves because each layer can be tested independently
Domain-Driven Design (DDD)
What is Domain-Driven Design?
Domain-Driven Design is a philosophy for building software that truly mirrors the real-world problem domain. It's about aligning code structure and language with how domain experts—business stakeholders, product managers, or domain specialists—actually think and talk about their work.
Core Concepts
1. Ubiquitous Language
Everyone involved speaks the same precise language. If a domain expert says "invoice," the code should have an Invoice entity. If they say "settlement," there shouldn't be three competing terms (Payment, Reconciliation, Transfer). This shared vocabulary collapses the wall between tech and business.
2. Bounded Contexts
Large systems are messy. DDD says: don't make one grand "domain model." Instead, divide the system into bounded contexts—self-contained conceptual zones where terms and rules are consistent.
For example:
- The Billing Context might define an Invoice differently than the Accounting Context does
- Each bounded context can be implemented as its own module, service, or microservice
- Explicit integration contracts exist between contexts
3. The Model
Inside each bounded context, your classes, aggregates, and events model real-world behavior—not just data.
Key DDD Building Blocks:
- Entities — Have an identity over time (Order, User, Account)
- Value Objects — Immutable, identity-less values (Money, Email, Coordinates)
- Aggregates — Clusters of entities treated as a single consistency boundary (Order + its OrderItems)
- Domain Services — Operations that don't belong to any one entity (CurrencyConversionService)
- Domain Events — Things that happened and matter to the domain (PaymentReceived, TicketReserved)
- Repositories — Domain persistence contracts (not implementation details)
Why DDD is Suited to AI Code Generation
DDD focuses on a clear domain model using Entities, Value Objects, Domain Services, and Repositories—all expressed in the ubiquitous language of the business. This explicit modeling gives AI agents a well-defined vocabulary and structure to follow.
By avoiding "anemic" models (just data with getters/setters) and placing behavior inside domain objects instead of scattering business logic in controllers or utilities, DDD reduces ambiguity about where logic should go.
In practice, teams have found that DDD's emphasis on bounded contexts and consistent naming creates a shared vocabulary that an AI can apply reliably in code generation. As developers report: DDD principles were "extraordinarily effective when working with AI", almost as if the approach was "designed specifically for helping AI understand complex domains."
How DDD Improves AI Output
By defining the core concepts up front, you guide the AI to use the correct terms and relationships. For example, if you specify that your system has an Order aggregate with an addLineItem() method, the AI is less likely to invent a different pattern—it will follow the established method name and entity structure.
The predictability of DDD helps the AI "fill in the blanks" without deviating from intended design. In real-world usage, moving from a loose approach to a DDD-informed approach transformed AI-generated code from "disconnected, non-functional" snippets into "cohesive, working features that integrated properly with the codebase."
When to Use DDD
DDD thrives when:
- The domain itself is complex and evolving
- You have access to domain experts
- You're building something long-lived, not a quick MVP
- Business rules are nuanced and need to be captured clearly
- Your team values maintainability over speed
It's less ideal for:
- Simple CRUD applications
- Early exploratory projects without stable concepts
- Projects where business requirements are entirely unclear
TypeScript Example – Domain Model
Below is a simplified example of DDD style in TypeScript:
Value Object Example
/**
* Email value object
* Encapsulates email validation and behavior
*/
class Email {
readonly value: string;
constructor(value: string) {
if (!value.includes('@')) {
throw new Error('Invalid email format');
}
this.value = value;
}
get domain(): string {
return this.value.split('@')[1];
}
equals(other: Email): boolean {
return this.value === other.value;
}
}Entity Example
/**
* User entity
* Has identity and lifecycle, contains business behavior
*/
class User {
private _id: string;
private _email: Email;
private _name: string;
private _createdAt: Date;
constructor(email: Email, name: string) {
this._id = generateUniqueId(); // unique identity
this._email = email;
this._name = name;
this._createdAt = new Date();
}
/**
* Business rule: change email with validation
* This encapsulates business logic in the domain
*/
changeEmail(newEmail: Email): void {
if (!this.isEmailDomainAllowed(newEmail)) {
throw new Error('Email domain not allowed for this user type');
}
this._email = newEmail;
}
private isEmailDomainAllowed(email: Email): boolean {
const allowedDomains = ['company.com', 'company-partners.com'];
return allowedDomains.includes(email.domain);
}
get id(): string {
return this._id;
}
get email(): Email {
return this._email;
}
get name(): string {
return this._name;
}
}Repository Interface (Domain Layer)
/**
* UserRepository
* Domain persistence contract (no infrastructure details)
*/
interface UserRepository {
findByEmail(email: Email): Promise<User | null>;
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}Domain Service
/**
* UserRegistrationService
* Orchestrates the user registration process
* Belongs to domain, not infrastructure
*/
class UserRegistrationService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async register(email: string, name: string): Promise<User> {
// Create value object with validation
const emailVO = new Email(email);
// Check business rule: no duplicate emails
const existingUser = await this.userRepository.findByEmail(emailVO);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Create user entity with behavior
const newUser = new User(emailVO, name);
// Persist to repository
await this.userRepository.save(newUser);
// Send domain event (user registered)
await this.emailService.sendWelcomeEmail(newUser);
return newUser;
}
}DDD vs. Traditional MVC
Traditional MVC Approach
// ❌ Anemic model - just data
class User {
id: string;
email: string;
name: string;
}
// ❌ Business logic scattered in controller
app.post('/register', async (req, res) => {
const { email, name } = req.body;
// Validation mixed with persistence
if (!email.includes('@')) {
return res.status(400).send('Invalid email');
}
// Direct database access
const existing = await db.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (existing.rows.length > 0) {
return res.status(400).send('User exists');
}
// Insert directly
const result = await db.query(
'INSERT INTO users (email, name) VALUES (?, ?)',
[email, name]
);
res.status(201).json({ id: result.lastID, email, name });
});Problems:
- Business logic scattered across controller and database code
- Anemic model with no behavior
- Hard to test business rules in isolation
- AI tends to generate similar scattered logic if not guided to DDD
DDD Approach
// ✅ Rich domain model with behavior
class Email {
constructor(value: string) {
if (!value.includes('@')) throw new Error('Invalid email');
this.value = value;
}
}
class User {
constructor(email: Email, name: string) {
this.email = email;
this.name = name;
}
changeEmail(newEmail: Email): void {
this.email = newEmail; // Business rule encapsulated
}
}
// ✅ Domain service with clear responsibility
class RegisterUserService {
constructor(private repo: UserRepository) {}
async execute(email: string, name: string): Promise<User> {
const emailVO = new Email(email); // validation
if (await this.repo.findByEmail(emailVO)) {
throw new Error('User exists');
}
const user = new User(emailVO, name);
await this.repo.save(user);
return user;
}
}
// ✅ Clean controller delegates to domain
app.post('/register', async (req, res) => {
try {
const user = await registerService.execute(
req.body.email,
req.body.name
);
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});Benefits:
- Business logic isolated in domain layer
- Clear, testable entities with behavior
- Repository provides clear persistence contract
- AI naturally reuses patterns and follows established structure
Domain Events
Domain events capture important occurrences in your domain:
/**
* UserRegistered domain event
* Signals that a user was successfully registered
*/
interface DomainEvent {
eventId: string;
occurredAt: Date;
aggregateId: string;
aggregateType: string;
}
class UserRegisteredEvent implements DomainEvent {
eventId: string;
occurredAt: Date;
aggregateId: string;
aggregateType = 'User';
constructor(
public userId: string,
public email: string,
public name: string
) {
this.eventId = generateId();
this.occurredAt = new Date();
this.aggregateId = userId;
}
}
// In UserRegistrationService:
async execute(email: string, name: string): Promise<User> {
const emailVO = new Email(email);
const user = new User(emailVO, name);
await this.userRepository.save(user);
// Publish event
const event = new UserRegisteredEvent(user.id, email, name);
await this.eventBus.publish(event);
return user;
}File Structure for DDD
src/
├── domain/
│ ├── entities/
│ │ ├── User.ts
│ │ ├── Order.ts
│ │ └── Account.ts
│ ├── value-objects/
│ │ ├── Email.ts
│ │ ├── Money.ts
│ │ └── Address.ts
│ ├── services/
│ │ ├── UserRegistrationService.ts
│ │ └── OrderProcessingService.ts
│ ├── repositories/
│ │ ├── UserRepository.ts
│ │ └── OrderRepository.ts
│ └── events/
│ ├── DomainEvent.ts
│ ├── UserRegisteredEvent.ts
│ └── OrderPlacedEvent.ts
├── application/
│ ├── use-cases/
│ │ ├── RegisterUserUseCase.ts
│ │ └── PlaceOrderUseCase.ts
│ └── dto/
│ ├── RegisterUserRequest.ts
│ └── PlaceOrderRequest.ts
├── infrastructure/
│ ├── repositories/
│ │ ├── MongoUserRepository.ts
│ │ └── PostgresOrderRepository.ts
│ ├── events/
│ │ └── KafkaEventBus.ts
│ └── http/
│ └── UserController.ts
└── interfaces/
└── api/
├── UserAPI.ts
└── OrderAPI.tsTips for AI-Generated DDD Code
1. Define your domain model first - Show the AI your entities, value objects, and bounded contexts before asking for implementation 2. Use consistent naming - Apply ubiquitous language consistently in prompts 3. Describe business rules - Explain what the domain does and its constraints 4. Reference patterns - Point the AI to existing entities and services as examples 5. Request by aggregate - Ask for one aggregate at a time rather than the entire system
Key Takeaways
- DDD reduces ambiguity for AI through explicit domain modeling
- Business logic lives in domain entities, not scattered across layers
- Value objects enforce business rules at construction time
- Repositories provide clean persistence contracts
- Domain events capture important business occurrences
- The ubiquitous language becomes your shared vocabulary with both humans and AI
Hexagonal Architecture (Ports & Adapters)
What is Hexagonal Architecture?
Hexagonal Architecture, also known as Ports and Adapters, is closely related to Clean Architecture but emphasizes isolating external integrations. In Hexagonal design, your core business logic defines Port interfaces for any outside interaction (database, web services, message queues), and Adapter classes implement those interfaces to handle the external communication.
The core (inside the hexagon) doesn't depend on any external tech details—it only knows the ports.
┌─────────────────────────────┐
│ Web Controller Adapter │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ HTTP Port (Input) │
└──────────────┬──────────────┘
│
┌───────────────────┼────────────────────┐
│ │ │
│ ┌────────────────▼─────────────────┐ │
│ │ │ │
│ │ CORE BUSINESS LOGIC │ │
│ │ │ │
│ └────────────────▲─────────────────┘ │
│ │ │
└───────────────────┼────────────────────┘
│
┌──────────────────┴──────────────────┐
│ │
┌──▼──────────────┐ ┌──────────▼──┐
│ Database Port │ │ API Port │
└──┬──────────────┘ └──────┬──────┘
│ │
┌──▼──────────────┐ ┌──────▼──────┐
│ MongoDB Adapter │ │ REST Adapter│
└─────────────────┘ └─────────────┘Why Hexagonal is AI-Friendly
This pattern provides a very clear blueprint for the AI whenever some external interaction is needed. Instead of letting the AI scatter API calls or SQL queries wherever, you can explicitly prompt:
- "Define a port interface for X"
- "Implement an adapter for X that uses technology Y"
AI coding agents can follow this pattern well, producing code that is easier to swap out or fix later. It also aligns with how AI "thinks" in terms of completing patterns—if the AI sees that every database access goes via a SomethingRepository interface, it will likely continue that pattern consistently.
Hexagonal Architecture thus reduces the cognitive load on the AI by standardizing how to extend the system. As developers note, Hexagonal and DDD together yield "clear patterns, separation of concerns, rich domain models," making the codebase highly maintainable and adaptable—properties beneficial not only for humans but for AI generating code as well.
Core Concepts
1. The Hexagon (Core)
The core contains pure business logic with no knowledge of external systems. It's where all the important domain rules live.
/**
* Core business logic
* No external dependencies
*/
class UserAccount {
private _id: string;
private _email: Email;
private _balance: Money;
private _status: AccountStatus;
constructor(email: Email) {
this._id = generateId();
this._email = email;
this._balance = Money.zero();
this._status = AccountStatus.ACTIVE;
}
deposit(amount: Money): void {
if (amount.isNegative()) {
throw new Error('Cannot deposit negative amount');
}
this._balance = this._balance.add(amount);
}
withdraw(amount: Money): void {
if (amount.isGreaterThan(this._balance)) {
throw new Error('Insufficient funds');
}
this._balance = this._balance.subtract(amount);
}
// Pure business logic - no I/O
getAvailableBalance(): Money {
return this._balance;
}
get id(): string {
return this._id;
}
get email(): Email {
return this._email;
}
}2. Ports (Interfaces)
Ports define contracts for external interactions. They're part of the core, but implementation lives outside.
/**
* Port: defines what the core needs from persistence
* Lives in domain layer, but no implementation details
*/
interface AccountRepository {
findById(id: string): Promise<UserAccount | null>;
save(account: UserAccount): Promise<void>;
delete(id: string): Promise<void>;
}
/**
* Port: defines what the core needs from external API
*/
interface NotificationPort {
sendEmail(to: string, subject: string, body: string): Promise<void>;
}
/**
* Port: defines what the core needs from an external payment processor
*/
interface PaymentGatewayPort {
processPayment(amount: Money, accountId: string): Promise<PaymentResult>;
}3. Adapters (Implementations)
Adapters implement ports and handle the messy details of external systems.
/**
* Adapter: Implements repository port using MongoDB
*/
class MongoAccountRepository implements AccountRepository {
constructor(private mongoClient: MongoClient) {}
async findById(id: string): Promise<UserAccount | null> {
const doc = await this.mongoClient.collection('accounts').findOne({ _id: id });
if (!doc) {
return null;
}
// Reconstruct core domain object
const account = new UserAccount(new Email(doc.email));
doc.balance && account.deposit(Money.fromCents(doc.balance));
return account;
}
async save(account: UserAccount): Promise<void> {
const document = {
_id: account.id,
email: account.email.value,
balance: account.getAvailableBalance().cents(),
savedAt: new Date()
};
await this.mongoClient.collection('accounts').updateOne(
{ _id: account.id },
{ $set: document },
{ upsert: true }
);
}
async delete(id: string): Promise<void> {
await this.mongoClient.collection('accounts').deleteOne({ _id: id });
}
}
/**
* Adapter: Implements notification port using SendGrid
*/
class SendGridNotificationAdapter implements NotificationPort {
constructor(private sendgridClient: SendGridClient) {}
async sendEmail(to: string, subject: string, body: string): Promise<void> {
const message = {
to,
from: 'noreply@company.com',
subject,
text: body
};
await this.sendgridClient.send(message);
}
}
/**
* Adapter: Implements payment gateway port using Stripe
*/
class StripePaymentAdapter implements PaymentGatewayPort {
constructor(private stripeClient: StripeClient) {}
async processPayment(amount: Money, accountId: string): Promise<PaymentResult> {
try {
const charge = await this.stripeClient.charges.create({
amount: amount.cents(),
currency: 'usd',
idempotencyKey: accountId // Prevent duplicate charges
});
return {
success: true,
transactionId: charge.id,
timestamp: new Date()
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date()
};
}
}
}Application Layer: Use Cases with Ports
/**
* Use case: Deposit money into account
* Uses ports (no knowledge of specific implementations)
*/
class DepositMoneyUseCase {
constructor(
private accountRepository: AccountRepository,
private notificationPort: NotificationPort
) {}
async execute(accountId: string, amount: Money): Promise<AccountDTO> {
// Get account from repository (abstracted)
const account = await this.accountRepository.findById(accountId);
if (!account) {
throw new AccountNotFoundError(accountId);
}
// Apply business rule
account.deposit(amount);
// Persist (abstracted - could be MongoDB, PostgreSQL, etc.)
await this.accountRepository.save(account);
// Notify (abstracted - could be email, SMS, etc.)
await this.notificationPort.sendEmail(
account.email.value,
'Deposit Received',
`Your deposit of $${amount.dollars()} has been processed.`
);
return new AccountDTO(account);
}
}Real-World Example: Order Processing
Define Ports
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
interface ShippingServicePort {
calculateShippingCost(destination: Address): Promise<Money>;
scheduleShipment(orderId: string, destination: Address): Promise<string>;
}
interface InventoryServicePort {
reserveItems(orderId: string, items: OrderItem[]): Promise<void>;
releaseReservation(orderId: string): Promise<void>;
}
interface PaymentProcessorPort {
charge(amount: Money, paymentMethod: string): Promise<TransactionId>;
refund(transactionId: string): Promise<void>;
}Core Domain Logic
class Order {
private _id: string;
private _items: OrderItem[] = [];
private _status: OrderStatus = OrderStatus.PENDING;
private _destination: Address;
private _shippingCost: Money = Money.zero();
constructor(destination: Address) {
this._id = generateId();
this._destination = destination;
}
addItem(item: OrderItem): void {
if (this._status !== OrderStatus.PENDING) {
throw new Error('Cannot modify confirmed order');
}
this._items.push(item);
}
setShippingCost(cost: Money): void {
this._shippingCost = cost;
}
calculateTotal(): Money {
const itemsTotal = this._items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero()
);
return itemsTotal.add(this._shippingCost);
}
confirm(): void {
if (this._items.length === 0) {
throw new Error('Cannot confirm empty order');
}
this._status = OrderStatus.CONFIRMED;
}
markAsShipped(): void {
if (this._status !== OrderStatus.CONFIRMED) {
throw new Error('Only confirmed orders can be shipped');
}
this._status = OrderStatus.SHIPPED;
}
// Getters...
get id(): string { return this._id; }
get status(): OrderStatus { return this._status; }
get items(): OrderItem[] { return [...this._items]; }
}Use Case Using Ports
class PlaceOrderUseCase {
constructor(
private orderRepository: OrderRepository,
private shippingService: ShippingServicePort,
private inventoryService: InventoryServicePort,
private paymentProcessor: PaymentProcessorPort
) {}
async execute(
destination: Address,
items: OrderItem[],
paymentMethod: string
): Promise<OrderDTO> {
// 1. Create order
const order = new Order(destination);
items.forEach(item => order.addItem(item));
// 2. Calculate shipping (uses port - could be different implementations)
const shippingCost = await this.shippingService.calculateShippingCost(destination);
order.setShippingCost(shippingCost);
// 3. Reserve inventory (uses port - abstracted)
try {
await this.inventoryService.reserveItems(order.id, items);
} catch (error) {
throw new InventoryNotAvailableError(error.message);
}
// 4. Process payment (uses port - abstracted)
let transactionId: string;
try {
transactionId = await this.paymentProcessor.charge(
order.calculateTotal(),
paymentMethod
);
} catch (error) {
// Rollback reservation
await this.inventoryService.releaseReservation(order.id);
throw new PaymentFailedError(error.message);
}
// 5. Confirm order with business rules
order.confirm();
// 6. Schedule shipment (uses port - abstracted)
const trackingNumber = await this.shippingService.scheduleShipment(
order.id,
destination
);
order.setTrackingNumber(trackingNumber);
// 7. Persist (uses port - abstracted)
await this.orderRepository.save(order);
return new OrderDTO(order);
}
}Implement Adapters for Different Technologies
// Switch databases without changing core logic
class PostgresOrderRepository implements OrderRepository {
constructor(private db: Database) {}
async save(order: Order): Promise<void> {
await this.db.query('INSERT INTO orders ...', [order.id, ...]);
}
}
class MockOrderRepository implements OrderRepository {
private orders = new Map<string, Order>();
async save(order: Order): Promise<void> {
this.orders.set(order.id, order);
}
async findById(id: string): Promise<Order | null> {
return this.orders.get(id) ?? null;
}
}
// Switch shipping providers without changing core logic
class FedExShippingAdapter implements ShippingServicePort {
constructor(private fedexClient: FedExAPI) {}
async calculateShippingCost(destination: Address): Promise<Money> {
const quote = await this.fedexClient.getRates(destination);
return Money.fromCents(quote.standardShippingCents);
}
async scheduleShipment(orderId: string, destination: Address): Promise<string> {
const shipment = await this.fedexClient.createShipment({
orderId,
destination: destination.toFedExFormat()
});
return shipment.trackingNumber;
}
}
class UPSShippingAdapter implements ShippingServicePort {
constructor(private upsClient: UPSAPI) {}
async calculateShippingCost(destination: Address): Promise<Money> {
const quote = await this.upsClient.getRate(destination);
return Money.fromCents(quote.groundRate);
}
async scheduleShipment(orderId: string, destination: Address): Promise<string> {
const shipment = await this.upsClient.schedulePickup({
packages: [{ orderId }],
destination
});
return shipment.trackingCode;
}
}Testability Benefits
The core logic can be tested without any external dependencies:
describe('PlaceOrderUseCase', () => {
let useCase: PlaceOrderUseCase;
let mockOrderRepo: MockOrderRepository;
let mockShippingService: MockShippingService;
let mockInventoryService: MockInventoryService;
let mockPaymentProcessor: MockPaymentProcessor;
beforeEach(() => {
// Use mock implementations
mockOrderRepo = new MockOrderRepository();
mockShippingService = new MockShippingService();
mockInventoryService = new MockInventoryService();
mockPaymentProcessor = new MockPaymentProcessor();
useCase = new PlaceOrderUseCase(
mockOrderRepo,
mockShippingService,
mockInventoryService,
mockPaymentProcessor
);
});
it('should place order successfully with all steps', async () => {
const destination = new Address('123 Main', 'Springfield', 'IL', '62701');
const items = [new OrderItem('PROD-1', 2, Money.fromDollars(10))];
const result = await useCase.execute(destination, items, 'card-token');
expect(result.status).toBe(OrderStatus.CONFIRMED);
expect(mockOrderRepo.saved()).toBe(1);
expect(mockPaymentProcessor.chargesCalled()).toBe(1);
});
it('should rollback inventory when payment fails', async () => {
mockPaymentProcessor.simulateFailure('Card declined');
const destination = new Address('123 Main', 'Springfield', 'IL', '62701');
const items = [new OrderItem('PROD-1', 2, Money.fromDollars(10))];
await expect(
useCase.execute(destination, items, 'invalid-card')
).rejects.toThrow(PaymentFailedError);
expect(mockInventoryService.reservationsCancelled()).toBe(1);
});
});File Structure for Hexagonal Architecture
src/
├── core/
│ ├── entities/
│ │ ├── Order.ts
│ │ ├── Account.ts
│ │ └── User.ts
│ ├── ports/
│ │ ├── OrderRepository.ts
│ │ ├── ShippingServicePort.ts
│ │ ├── PaymentProcessorPort.ts
│ │ └── NotificationPort.ts
│ ├── use-cases/
│ │ ├── PlaceOrderUseCase.ts
│ │ ├── ConfirmOrderUseCase.ts
│ │ └── CancelOrderUseCase.ts
│ ├── value-objects/
│ │ ├── Money.ts
│ │ ├── Address.ts
│ │ └── OrderStatus.ts
│ └── errors/
│ └── DomainError.ts
├── adapters/
│ ├── repositories/
│ │ ├── MongoOrderRepository.ts
│ │ ├── PostgresOrderRepository.ts
│ │ └── MockOrderRepository.ts
│ ├── shipping/
│ │ ├── FedExShippingAdapter.ts
│ │ ├── UPSShippingAdapter.ts
│ │ └── MockShippingService.ts
│ ├── payment/
│ │ ├── StripePaymentAdapter.ts
│ │ ├── PayPalPaymentAdapter.ts
│ │ └── MockPaymentProcessor.ts
│ └── notification/
│ ├── SendGridNotificationAdapter.ts
│ ├── TwilioSmsAdapter.ts
│ └── MockNotificationService.ts
├── controllers/
│ └── OrderController.ts
├── config/
│ └── dependencies.ts
└── main.tsDependency Injection for Hexagonal
/**
* Composition root
* Wire up core with specific adapters
*/
class ApplicationFactory {
static createOrderController(config: AppConfig): OrderController {
// Create adapters based on configuration
const orderRepository = config.database === 'mongodb'
? new MongoOrderRepository(config.mongoUrl)
: new PostgresOrderRepository(config.postgresUrl);
const shippingService = config.shippingProvider === 'fedex'
? new FedExShippingAdapter(config.fedexKey)
: new UPSShippingAdapter(config.upsKey);
const paymentProcessor = config.paymentProvider === 'stripe'
? new StripePaymentAdapter(config.stripeKey)
: new PayPalPaymentAdapter(config.paypalKey);
const notificationService = new SendGridNotificationAdapter(
config.sendgridKey
);
// Inject into use case
const placeOrderUseCase = new PlaceOrderUseCase(
orderRepository,
shippingService,
new RealInventoryService(),
paymentProcessor
);
// Return controller with use case
return new OrderController(placeOrderUseCase);
}
static createTestController(): OrderController {
// For testing, use mock adapters
const orderRepository = new MockOrderRepository();
const shippingService = new MockShippingService();
const paymentProcessor = new MockPaymentProcessor();
const notificationService = new MockNotificationService();
const placeOrderUseCase = new PlaceOrderUseCase(
orderRepository,
shippingService,
new MockInventoryService(),
paymentProcessor
);
return new OrderController(placeOrderUseCase);
}
}Tips for AI-Generated Hexagonal Code
1. Start with core - Have AI generate domain entities first 2. Define ports upfront - Create all port interfaces before asking for adapters 3. Generate one adapter at a time - Ask AI to implement each adapter separately 4. Show pattern examples - Reference one adapter implementation when asking for others 5. Use mocks for testing - Ask AI to create mock implementations alongside real ones 6. Inject dependencies - Use constructor injection rather than globals or singletons
Key Takeaways
- Core logic is isolated from external systems
- Ports define contracts that adapters implement
- Easy to swap implementations without changing core
- Highly testable because core can use mock adapters
- AI can focus on one adapter at a time
- Clear separation between core logic and infrastructure
- Flexibility to support multiple external systems simultaneously
Page Object Model (POM) for Testing
What is Page Object Model?
The Page Object Model (POM) is a design pattern for creating object-oriented representations of pages or components in your web application. Each page is represented as a class that encapsulates:
- Locators for UI elements (selectors)
- Actions that can be performed on the page (methods)
- Data validation specific to that page
Tests then use these page objects instead of directly interacting with raw HTML elements or Playwright/Cypress commands.
Structure
Page (Login)
├── Locators
│ ├── usernameInput = '#username'
│ ├── passwordInput = '#password'
│ └── submitButton = 'button[type="submit"]'
├── Actions
│ ├── enterUsername(username)
│ ├── enterPassword(password)
│ ├── submit()
│ └── loginAs(username, password)
└── Assertions
├── isErrorDisplayed()
└── getErrorMessage()Why POM Helps AI in Testing
POM introduces clarity and separability in test code—very similar to how Clean Architecture does for app code. Instead of an AI writing a monolithic script with a sequence of clicks and assertions, it can first generate a LoginPage class with methods, then write test scenarios that call those methods.
Multiple Benefits
1. Clarity & Readability
Test scripts become concise and intent-focused:
// ❌ Without POM - Hard to understand intent
test('user can log in', async ({ page }) => {
await page.fill('#username', 'alice');
await page.fill('#password', 'pass123');
await page.click('button[type="submit"]');
await page.waitForNavigation();
const welcomeText = await page.textContent('.welcome-msg');
expect(welcomeText).toContain('Welcome, Alice');
});
// ✅ With POM - Clear intent
test('user can log in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.loginAs('alice', 'pass123');
await expect(page).toHaveURL(/.*\/dashboard/);
expect(await loginPage.getWelcomeMessage()).toContain('Alice');
});Copilot and ChatGPT are good at completing boilerplate; with POM, they can suggest the repetitive parts (selectors and methods) in a structured way.
2. Reduced Duplication
The AI doesn't have to rewrite the logic to click the "Login" button in every test—it generates it once in the page object. This ensures consistency and reduces maintenance.
3. Leverage AI Capabilities
Modern AI coding tools are surprisingly adept at POM-style generation. GitHub Copilot and ChatGPT can generate boilerplate POM classes and entire test functions from context. You can prompt: "write a test that logs in and checks welcome message" and it will utilize the page object's methods correctly.
4. Maintainability
If the UI changes, you update the page object class, and all tests benefit automatically.
Playwright Example
Step 1: Create Page Object
/**
* LoginPage
* Represents the login page and encapsulates its interactions
*/
export class LoginPage {
// Locators
private readonly usernameInput = '#username';
private readonly passwordInput = '#password';
private readonly submitButton = 'button[type="submit"]';
private readonly errorMessage = '.error-message';
private readonly rememberMeCheckbox = '#remember-me';
constructor(private page: Page) {}
/**
* Enter username into the username field
*/
async enterUsername(username: string): Promise<void> {
await this.page.fill(this.usernameInput, username);
}
/**
* Enter password into the password field
*/
async enterPassword(password: string): Promise<void> {
await this.page.fill(this.passwordInput, password);
}
/**
* Click the submit/login button
*/
async submit(): Promise<void> {
await this.page.click(this.submitButton);
}
/**
* Higher-level action combining login steps
*/
async loginAs(username: string, password: string): Promise<void> {
await this.enterUsername(username);
await this.enterPassword(password);
await this.submit();
}
/**
* Variant: login with remember me option
*/
async loginWithRememberMe(username: string, password: string): Promise<void> {
await this.enterUsername(username);
await this.enterPassword(password);
await this.page.check(this.rememberMeCheckbox);
await this.submit();
}
/**
* Get error message text
*/
async getErrorMessage(): Promise<string | null> {
const errorElement = await this.page.$(this.errorMessage);
if (!errorElement) return null;
return await errorElement.textContent();
}
/**
* Check if error is displayed
*/
async isErrorDisplayed(): Promise<boolean> {
const errorElement = await this.page.$(this.errorMessage);
return !!errorElement;
}
/**
* Navigate to login page
*/
async goto(): Promise<void> {
await this.page.goto('/login');
}
}Step 2: Create Component Page Objects
/**
* NavigationBar
* Represents the main navigation component
*/
export class NavigationBar {
private readonly logoLink = '[data-testid="logo"]';
private readonly userMenuButton = '[data-testid="user-menu"]';
private readonly logoutButton = '[data-testid="logout"]';
constructor(private page: Page) {}
async clickLogo(): Promise<void> {
await this.page.click(this.logoLink);
}
async openUserMenu(): Promise<void> {
await this.page.click(this.userMenuButton);
}
async logout(): Promise<void> {
await this.openUserMenu();
await this.page.click(this.logoutButton);
}
async isUserMenuOpen(): Promise<boolean> {
return await this.page.isVisible('[role="menu"]');
}
}
/**
* DashboardPage
* Represents the main dashboard
*/
export class DashboardPage {
private readonly welcomeMessage = '.welcome-banner h1';
private readonly userGreeting = '[data-testid="user-greeting"]';
private readonly statsCard = '.stats-card';
constructor(private page: Page) {}
async getWelcomeMessage(): Promise<string> {
return await this.page.textContent(this.welcomeMessage) || '';
}
async getUserGreeting(): Promise<string> {
return await this.page.textContent(this.userGreeting) || '';
}
async getStatsCards(): Promise<string[]> {
const elements = await this.page.$$(this.statsCard);
const texts: string[] = [];
for (const element of elements) {
const text = await element.textContent();
if (text) texts.push(text);
}
return texts;
}
async isVisible(): Promise<boolean> {
return await this.page.isVisible(this.welcomeMessage);
}
}Step 3: Write Tests Using Page Objects
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { NavigationBar } from './pages/NavigationBar';
test.describe('Authentication', () => {
test('user can log in with valid credentials', async ({ page }) => {
// Arrange
const loginPage = new LoginPage(page);
await loginPage.goto();
// Act
await loginPage.loginAs('alice@example.com', 'SecurePassword123!');
// Assert
await expect(page).toHaveURL(/.*\/dashboard/);
// Verify dashboard loaded
const dashboard = new DashboardPage(page);
expect(await dashboard.isVisible()).toBeTruthy();
expect(await dashboard.getWelcomeMessage()).toContain('Alice');
});
test('user sees error with invalid password', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.loginAs('alice@example.com', 'WrongPassword');
expect(await loginPage.isErrorDisplayed()).toBeTruthy();
expect(await loginPage.getErrorMessage()).toContain('Invalid credentials');
});
test('user can log in and log out', async ({ page }) => {
const loginPage = new LoginPage(page);
const navBar = new NavigationBar(page);
await loginPage.goto();
await loginPage.loginAs('alice@example.com', 'SecurePassword123!');
// Verify logged in
await expect(page).toHaveURL(/.*\/dashboard/);
// Log out
await navBar.logout();
// Verify logged out
await expect(page).toHaveURL(/.*\/login/);
});
test('user can log in with remember me option', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.loginWithRememberMe('alice@example.com', 'SecurePassword123!');
// Verify session persisted (cookie check)
const cookies = await page.context().cookies();
const hasSessionCookie = cookies.some(c => c.name === 'session_token');
expect(hasSessionCookie).toBeTruthy();
});
});
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Log in before each dashboard test
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.loginAs('alice@example.com', 'SecurePassword123!');
});
test('dashboard displays user stats', async ({ page }) => {
const dashboard = new DashboardPage(page);
const stats = await dashboard.getStatsCards();
expect(stats.length).toBeGreaterThan(0);
expect(stats[0]).toContain('Total Users'); // Example assertion
});
test('user greeting is personalized', async ({ page }) => {
const dashboard = new DashboardPage(page);
const greeting = await dashboard.getUserGreeting();
expect(greeting).toContain('Alice');
});
});Advanced POM Patterns
Composable Page Objects
/**
* Modal Page Object
* Represents a reusable modal component
*/
export class Modal {
private readonly backdrop = '.modal-backdrop';
private readonly closeButton = '.modal-close';
private readonly title = '.modal-title';
constructor(private page: Page) {}
async close(): Promise<void> {
await this.page.click(this.closeButton);
}
async getTitle(): Promise<string> {
return await this.page.textContent(this.title) || '';
}
async isVisible(): Promise<boolean> {
return await this.page.isVisible(this.backdrop);
}
}
/**
* DeleteConfirmationModal
* Extends Modal for delete confirmation
*/
export class DeleteConfirmationModal extends Modal {
private readonly confirmButton = '[data-testid="confirm-delete"]';
private readonly cancelButton = '[data-testid="cancel-delete"]';
private readonly warningMessage = '.warning-message';
async confirm(): Promise<void> {
await this.page.click(this.confirmButton);
}
async cancel(): Promise<void> {
await this.page.click(this.cancelButton);
}
async getWarningMessage(): Promise<string> {
return await this.page.textContent(this.warningMessage) || '';
}
}
// Usage in test
test('user can delete with confirmation', async ({ page }) => {
const listPage = new ItemListPage(page);
await listPage.goto();
await listPage.deleteItem(0); // Opens modal
const modal = new DeleteConfirmationModal(page);
expect(await modal.isVisible()).toBeTruthy();
expect(await modal.getWarningMessage()).toContain('Cannot be undone');
await modal.confirm();
// Verify item deleted
});Page Objects with Accessibility Testing
/**
* AccessibleLoginPage
* Includes accessibility assertions
*/
export class AccessibleLoginPage extends LoginPage {
async verifyAccessibility(): Promise<void> {
// Verify labels are associated with inputs
const usernameLabel = await this.page.getAttribute(
this.usernameInput,
'aria-label'
);
expect(usernameLabel).toBeTruthy();
// Verify button is keyboard accessible
const button = await this.page.$('button[type="submit"]');
expect(await button?.getAttribute('tabindex')).not.toBe('-1');
}
async navigateWithKeyboard(): Promise<void> {
// Tab to username field
await this.page.keyboard.press('Tab');
await this.enterUsername('alice');
// Tab to password field
await this.page.keyboard.press('Tab');
await this.enterPassword('password');
// Tab to submit button
await this.page.keyboard.press('Tab');
// Submit with Enter key
await this.page.keyboard.press('Enter');
}
}Cypress Example
/**
* LoginPage for Cypress
*/
export class CypressLoginPage {
// Locators using Cypress selectors
private username = '[data-cy="username-input"]';
private password = '[data-cy="password-input"]';
private submit = '[data-cy="login-submit"]';
private error = '[data-cy="error-message"]';
loginAs(username: string, password: string): void {
cy.get(this.username).type(username);
cy.get(this.password).type(password);
cy.get(this.submit).click();
}
getErrorMessage(): Chainable<string> {
return cy.get(this.error).invoke('text');
}
isErrorDisplayed(): Chainable<number> {
return cy.get(this.error).its('length');
}
}
// Usage
describe('Login', () => {
it('user can login', () => {
const loginPage = new CypressLoginPage();
cy.visit('/login');
loginPage.loginAs('alice@example.com', 'password123');
cy.url().should('include', '/dashboard');
});
});File Structure for POM Tests
tests/
├── e2e/
│ ├── auth.spec.ts
│ ├── dashboard.spec.ts
│ └── user-profile.spec.ts
├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ ├── UserProfilePage.ts
│ ├── components/
│ │ ├── NavigationBar.ts
│ │ ├── Modal.ts
│ │ └── DataTable.ts
│ └── base/
│ └── BasePage.ts
├── fixtures/
│ ├── users.json
│ └── test-data.ts
└── helpers/
├── test-utils.ts
└── api-helpers.tsBest Practices
1. Single Responsibility
Each page object should represent one logical page or component:
// ✅ Good - One page object
export class ProductListPage { }
// ❌ Avoid - Too many responsibilities
export class ProductListAndDetailPage { }2. Encapsulate Locators
Keep selectors private and expose actions:
// ✅ Good
export class LoginPage {
private readonly usernameInput = '#username'; // Private
async enterUsername(value: string) { } // Public action
}
// ❌ Avoid
export class LoginPage {
usernameInput = '#username'; // Exposed locator
}3. Use Data Test IDs
Prefer stable selectors over CSS/XPath that might break with styling:
// ✅ Good
private readonly submitButton = '[data-testid="login-submit"]';
// ❌ Fragile
private readonly submitButton = 'form > div > button.btn.btn-primary';4. Return Page Objects for Navigation
When actions navigate to another page, return a new page object:
async login(): Promise<DashboardPage> {
await this.submit();
return new DashboardPage(this.page);
}
// Usage
const dashboard = await loginPage.login();
expect(await dashboard.getWelcomeMessage()).toContain('Welcome');5. Wait for Elements
Always wait appropriately:
// ✅ Good
async getUserGreeting(): Promise<string> {
await this.page.waitForSelector(this.userGreeting);
return await this.page.textContent(this.userGreeting) || '';
}
// ❌ Unreliable
async getUserGreeting(): Promise<string> {
return await this.page.textContent(this.userGreeting) || '';
}Tips for AI-Generated POM Code
1. Start with page structure - Define all locators first 2. Create action methods - High-level methods (loginAs) before low-level (enterUsername) 3. Add assertions - Include verification methods in page objects 4. Use inheritance - Create BasePage for common functionality 5. Generate tests from POMs - Once pages are defined, tests follow naturally 6. Name methods by intent - Use "loginAs" not "fillAndClick"
Common Mistakes to Avoid
❌ Mixing Test Logic with Page Objects
// Wrong - test logic in page object
async loginAndVerifyDashboard() {
await this.loginAs('user', 'pass');
expect(await this.page.url()).toContain('/dashboard');
}✅ Keep Page Objects Pure
// Right - page object just interacts, test does assertions
async login() {
await this.loginAs('user', 'pass');
}
// In test:
await loginPage.login();
expect(page.url()).toContain('/dashboard');❌ Hardcoding Test Data
// Wrong
async login() {
await this.loginAs('testuser@example.com', 'hardcodedPassword');
}✅ Pass Test Data to Methods
// Right
async login(username: string, password: string) {
await this.loginAs(username, password);
}
// In test:
await loginPage.login('testuser@example.com', 'password123');Key Takeaways
- Page Objects encapsulate UI interactions making tests more readable
- High-level action methods (like
loginAs) hide implementation details - Reduced duplication when same actions are needed in multiple tests
- Easier to maintain - UI changes only require updating page objects
- AI naturally generates POM-style code when shown examples
- Composable - Page objects can extend or use other page objects
- Testable - Page objects themselves can be unit tested
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-16T14:26:03.324Z",
"slug": "adammanuel-dev-architecture-patterns",
"source_url": "https://github.com/AdamManuel-dev/claude-code-ext/tree/main/skills/architecture-patterns",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "1e5c18b429463c82a0598f04077c937e89d5ec5cdc3a23e4b496eef625b3fb17",
"tree_hash": "c79bfa0ddb46e763cf406e88908e590b5f5efcbd0164e0f844d5c14203747c0c"
},
"skill": {
"name": "architecture-patterns",
"description": "AI-friendly architecture patterns for TypeScript projects including Domain-Driven Design, Clean Architecture, Hexagonal Architecture, and Page Object Model testing patterns",
"summary": "AI-friendly architecture patterns for TypeScript projects including Domain-Driven Design, Clean Arch...",
"icon": "🏗️",
"version": "1.0.0",
"author": "AdamManuel-dev",
"license": "MIT",
"category": "coding",
"tags": [
"architecture",
"typescript",
"domain-driven-design",
"clean-code",
"testing"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": [
"external_commands",
"network"
]
},
"security_audit": {
"risk_level": "safe",
"is_blocked": false,
"safe_to_publish": true,
"summary": "This is a pure documentation skill containing only markdown files with architectural guidance. No executable code, network calls, file system access, or external commands exist. All 232 static findings are false positives: the scanner detected markdown code formatting (backticks) and documentation references to software concepts as security issues.",
"risk_factor_evidence": [
{
"factor": "external_commands",
"evidence": [
{
"file": "clean-architecture.md",
"line_start": 13,
"line_end": 24
},
{
"file": "clean-architecture.md",
"line_start": 24,
"line_end": 67
},
{
"file": "clean-architecture.md",
"line_start": 67,
"line_end": 129
},
{
"file": "clean-architecture.md",
"line_start": 129,
"line_end": 135
},
{
"file": "clean-architecture.md",
"line_start": 135,
"line_end": 186
},
{
"file": "clean-architecture.md",
"line_start": 186,
"line_end": 192
},
{
"file": "clean-architecture.md",
"line_start": 192,
"line_end": 243
},
{
"file": "clean-architecture.md",
"line_start": 243,
"line_end": 249
},
{
"file": "clean-architecture.md",
"line_start": 249,
"line_end": 325
},
{
"file": "clean-architecture.md",
"line_start": 325,
"line_end": 329
},
{
"file": "clean-architecture.md",
"line_start": 329,
"line_end": 382
},
{
"file": "clean-architecture.md",
"line_start": 382,
"line_end": 386
},
{
"file": "clean-architecture.md",
"line_start": 386,
"line_end": 424
},
{
"file": "clean-architecture.md",
"line_start": 424,
"line_end": 430
},
{
"file": "clean-architecture.md",
"line_start": 430,
"line_end": 469
},
{
"file": "clean-architecture.md",
"line_start": 469,
"line_end": 473
},
{
"file": "clean-architecture.md",
"line_start": 473,
"line_end": 504
},
{
"file": "clean-architecture.md",
"line_start": 504,
"line_end": 518
},
{
"file": "clean-architecture.md",
"line_start": 518,
"line_end": 537
},
{
"file": "clean-architecture.md",
"line_start": 537,
"line_end": 541
},
{
"file": "clean-architecture.md",
"line_start": 541,
"line_end": 565
},
{
"file": "clean-architecture.md",
"line_start": 565,
"line_end": 569
},
{
"file": "clean-architecture.md",
"line_start": 569,
"line_end": 581
},
{
"file": "ddd.md",
"line_start": 45,
"line_end": 45
},
{
"file": "ddd.md",
"line_start": 71,
"line_end": 94
},
{
"file": "ddd.md",
"line_start": 94,
"line_end": 98
},
{
"file": "ddd.md",
"line_start": 98,
"line_end": 144
},
{
"file": "ddd.md",
"line_start": 144,
"line_end": 148
},
{
"file": "ddd.md",
"line_start": 148,
"line_end": 159
},
{
"file": "ddd.md",
"line_start": 159,
"line_end": 163
},
{
"file": "ddd.md",
"line_start": 163,
"line_end": 197
},
{
"file": "ddd.md",
"line_start": 197,
"line_end": 203
},
{
"file": "ddd.md",
"line_start": 203,
"line_end": 237
},
{
"file": "ddd.md",
"line_start": 237,
"line_end": 247
},
{
"file": "ddd.md",
"line_start": 247,
"line_end": 294
},
{
"file": "ddd.md",
"line_start": 294,
"line_end": 306
},
{
"file": "ddd.md",
"line_start": 306,
"line_end": 347
},
{
"file": "ddd.md",
"line_start": 347,
"line_end": 351
},
{
"file": "ddd.md",
"line_start": 351,
"line_end": 391
},
{
"file": "hexagonal-architecture.md",
"line_start": 9,
"line_end": 37
},
{
"file": "hexagonal-architecture.md",
"line_start": 37,
"line_end": 46
},
{
"file": "hexagonal-architecture.md",
"line_start": 46,
"line_end": 56
},
{
"file": "hexagonal-architecture.md",
"line_start": 56,
"line_end": 101
},
{
"file": "hexagonal-architecture.md",
"line_start": 101,
"line_end": 107
},
{
"file": "hexagonal-architecture.md",
"line_start": 107,
"line_end": 131
},
{
"file": "hexagonal-architecture.md",
"line_start": 131,
"line_end": 137
},
{
"file": "hexagonal-architecture.md",
"line_start": 137,
"line_end": 223
},
{
"file": "hexagonal-architecture.md",
"line_start": 223,
"line_end": 227
},
{
"file": "hexagonal-architecture.md",
"line_start": 227,
"line_end": 255
},
{
"file": "hexagonal-architecture.md",
"line_start": 255,
"line_end": 261
},
{
"file": "hexagonal-architecture.md",
"line_start": 261,
"line_end": 267
},
{
"file": "hexagonal-architecture.md",
"line_start": 267,
"line_end": 287
},
{
"file": "hexagonal-architecture.md",
"line_start": 287,
"line_end": 291
},
{
"file": "hexagonal-architecture.md",
"line_start": 291,
"line_end": 342
},
{
"file": "hexagonal-architecture.md",
"line_start": 342,
"line_end": 346
},
{
"file": "hexagonal-architecture.md",
"line_start": 346,
"line_end": 404
},
{
"file": "hexagonal-architecture.md",
"line_start": 404,
"line_end": 408
},
{
"file": "hexagonal-architecture.md",
"line_start": 408,
"line_end": 464
},
{
"file": "hexagonal-architecture.md",
"line_start": 464,
"line_end": 470
},
{
"file": "hexagonal-architecture.md",
"line_start": 470,
"line_end": 517
},
{
"file": "hexagonal-architecture.md",
"line_start": 517,
"line_end": 521
},
{
"file": "hexagonal-architecture.md",
"line_start": 521,
"line_end": 565
},
{
"file": "hexagonal-architecture.md",
"line_start": 565,
"line_end": 569
},
{
"file": "hexagonal-architecture.md",
"line_start": 569,
"line_end": 622
},
{
"file": "page-object-model.md",
"line_start": 15,
"line_end": 29
},
{
"file": "page-object-model.md",
"line_start": 29,
"line_end": 41
},
{
"file": "page-object-model.md",
"line_start": 41,
"line_end": 59
},
{
"file": "page-object-model.md",
"line_start": 59,
"line_end": 79
},
{
"file": "page-object-model.md",
"line_start": 79,
"line_end": 158
},
{
"file": "page-object-model.md",
"line_start": 158,
"line_end": 162
},
{
"file": "page-object-model.md",
"line_start": 162,
"line_end": 225
},
{
"file": "page-object-model.md",
"line_start": 225,
"line_end": 229
},
{
"file": "page-object-model.md",
"line_start": 229,
"line_end": 316
},
{
"file": "page-object-model.md",
"line_start": 316,
"line_end": 322
},
{
"file": "page-object-model.md",
"line_start": 322,
"line_end": 383
},
{
"file": "page-object-model.md",
"line_start": 383,
"line_end": 387
},
{
"file": "page-object-model.md",
"line_start": 387,
"line_end": 422
},
{
"file": "page-object-model.md",
"line_start": 422,
"line_end": 426
},
{
"file": "page-object-model.md",
"line_start": 426,
"line_end": 463
},
{
"file": "page-object-model.md",
"line_start": 463,
"line_end": 467
},
{
"file": "page-object-model.md",
"line_start": 467,
"line_end": 489
},
{
"file": "page-object-model.md",
"line_start": 489,
"line_end": 497
},
{
"file": "page-object-model.md",
"line_start": 497,
"line_end": 503
},
{
"file": "page-object-model.md",
"line_start": 503,
"line_end": 509
},
{
"file": "page-object-model.md",
"line_start": 509,
"line_end": 520
},
{
"file": "page-object-model.md",
"line_start": 520,
"line_end": 526
},
{
"file": "page-object-model.md",
"line_start": 526,
"line_end": 532
},
{
"file": "page-object-model.md",
"line_start": 532,
"line_end": 538
},
{
"file": "page-object-model.md",
"line_start": 538,
"line_end": 547
},
{
"file": "page-object-model.md",
"line_start": 547,
"line_end": 553
},
{
"file": "page-object-model.md",
"line_start": 553,
"line_end": 564
},
{
"file": "page-object-model.md",
"line_start": 564,
"line_end": 579
},
{
"file": "page-object-model.md",
"line_start": 579,
"line_end": 585
},
{
"file": "page-object-model.md",
"line_start": 585,
"line_end": 589
},
{
"file": "page-object-model.md",
"line_start": 589,
"line_end": 598
},
{
"file": "page-object-model.md",
"line_start": 598,
"line_end": 602
},
{
"file": "page-object-model.md",
"line_start": 602,
"line_end": 607
},
{
"file": "page-object-model.md",
"line_start": 607,
"line_end": 611
},
{
"file": "page-object-model.md",
"line_start": 611,
"line_end": 619
},
{
"file": "page-object-model.md",
"line_start": 619,
"line_end": 624
},
{
"file": "page-object-model.md",
"line_start": 138,
"line_end": 138
},
{
"file": "page-object-model.md",
"line_start": 147,
"line_end": 147
},
{
"file": "page-object-model.md",
"line_start": 212,
"line_end": 212
},
{
"file": "page-object-model.md",
"line_start": 402,
"line_end": 402
},
{
"file": "page-object-model.md",
"line_start": 79,
"line_end": 158
},
{
"file": "page-object-model.md",
"line_start": 162,
"line_end": 225
},
{
"file": "page-object-model.md",
"line_start": 387,
"line_end": 422
},
{
"file": "SKILL.md",
"line_start": 168,
"line_end": 168
},
{
"file": "SKILL.md",
"line_start": 169,
"line_end": 169
},
{
"file": "SKILL.md",
"line_start": 170,
"line_end": 170
},
{
"file": "SKILL.md",
"line_start": 171,
"line_end": 171
}
]
},
{
"factor": "network",
"evidence": [
{
"file": "skill-report.json",
"line_start": 6,
"line_end": 6
}
]
}
],
"critical_findings": [],
"high_findings": [],
"medium_findings": [],
"low_findings": [],
"dangerous_patterns": [],
"files_scanned": 6,
"total_lines": 2643,
"audit_model": "claude",
"audited_at": "2026-01-16T14:26:03.324Z"
},
"content": {
"user_title": "Apply architecture patterns for better AI code generation",
"value_statement": "AI code generation often produces inconsistent or poorly structured code when projects lack clear architectural guidance. This skill provides proven architecture patterns that give AI assistants clear boundaries and consistent structures, resulting in code that is easier to maintain, test, and extend.",
"seo_keywords": [
"architecture patterns",
"domain-driven design",
"clean architecture",
"hexagonal architecture",
"page object model",
"typescript patterns",
"AI code generation",
"Claude Code",
"software architecture",
"testable code"
],
"actual_capabilities": [
"Guide AI to structure TypeScript projects using Domain-Driven Design principles with entities, value objects, and repositories",
"Implement Clean Architecture layers with dependency inversion for testable and maintainable code",
"Create Hexagonal Architecture with ports and adapters to isolate core logic from external systems",
"Build Page Object Model test patterns for Playwright, Cypress, and Selenium automation",
"Apply consistent naming conventions and ubiquitous language across AI-generated code"
],
"limitations": [
"Does not generate code directly - provides patterns for AI to follow",
"Focus is on TypeScript and web application patterns",
"Requires understanding of TypeScript fundamentals to apply patterns"
],
"use_cases": [
{
"target_user": "Full-stack developers",
"title": "Structure new TypeScript projects",
"description": "Set up projects with Clean Architecture layers so AI generates well-organized code from the start"
},
{
"target_user": "QA engineers",
"title": "Create maintainable E2E tests",
"description": "Build Page Object Model patterns that AI can extend with consistent, readable test code"
},
{
"target_user": "Technical architects",
"title": "Define bounded contexts",
"description": "Establish DDD boundaries and ubiquitous language that guides AI across complex domains"
}
],
"prompt_templates": [
{
"title": "DDD Entity",
"scenario": "Create a domain entity",
"prompt": "Create a [domain] entity with value objects. Include validation rules, identity, and business methods. Use TypeScript with the domain-driven design pattern from the architecture-patterns skill."
},
{
"title": "Clean Use Case",
"scenario": "Implement an application use case",
"prompt": "Implement a [feature] use case following Clean Architecture. Create the domain entity, use case service, repository port, and controller. Dependencies should point inward."
},
{
"title": "Hexagonal Adapter",
"scenario": "Build an external adapter",
"prompt": "Implement a [technology] adapter for the [port] port interface. Follow hexagonal architecture pattern with the core defining the contract and the adapter handling external communication."
},
{
"title": "Page Object",
"scenario": "Create a test page object",
"prompt": "Create a Playwright page object for [page_name]. Include locators, action methods, and assertions. Follow the Page Object Model pattern from the architecture-patterns skill."
}
],
"output_examples": [
{
"input": "Create a User entity with email validation and a UserRepository interface",
"output": [
"Entity with identity and behavior",
"Value objects for business rules",
"Repository port (not implementation)",
"Clear separation of concerns"
]
},
{
"input": "Set up a new backend service with Clean Architecture layers",
"output": [
"Domain layer with business rules",
"Application layer with use cases",
"Interface adapters for HTTP",
"Infrastructure layer for database"
]
},
{
"input": "Write a Playwright test for the login page",
"output": [
"LoginPage class with locators",
"Action methods for username and password",
"Assertion methods for validation",
"Clean test scenario using the page object"
]
}
],
"best_practices": [
"Define domain model first before asking AI to implement features",
"Keep inner layers pure with no dependencies on outer layers",
"Use ports (interfaces) to define contracts between layers"
],
"anti_patterns": [
"Mixing business logic with HTTP handling in controllers",
"Having domain entities depend on database or framework code",
"Exposing domain objects directly as API responses (use DTOs)"
],
"faq": [
{
"question": "Which architecture pattern should I choose for my project?",
"answer": "Clean Architecture works for most applications. Use DDD for complex domains with evolving rules. Choose Hexagonal for systems with multiple external integrations. Use POM for E2E testing."
},
{
"question": "Do these patterns work with frameworks like NestJS or Express?",
"answer": "Yes. These patterns are framework-agnostic. The concepts apply to any TypeScript backend. NestJS follows similar layered patterns out of the box."
},
{
"question": "How do I combine patterns effectively?",
"answer": "Start with DDD for domain modeling, apply Clean Architecture for layering, use Hexagonal for external integrations, and implement POM for testing. They complement each other well."
},
{
"question": "Is my data safe when using this skill?",
"answer": "This is a prompt-only skill with no code execution. It only provides architectural guidance. No data is collected, stored, or transmitted."
},
{
"question": "How does this help AI generate better code?",
"answer": "Clear patterns reduce ambiguity. When AI sees a defined structure with interfaces, it follows consistent patterns rather than generating scattered code with mixed concerns."
},
{
"question": "How is this different from following official framework documentation?",
"answer": "Framework docs show how to use features. Architecture patterns show how to structure code for maintainability, testability, and AI comprehension. They work together."
}
]
},
"file_structure": [
{
"name": "clean-architecture.md",
"type": "file",
"path": "clean-architecture.md",
"lines": 591
},
{
"name": "ddd.md",
"type": "file",
"path": "ddd.md",
"lines": 409
},
{
"name": "hexagonal-architecture.md",
"type": "file",
"path": "hexagonal-architecture.md",
"lines": 642
},
{
"name": "page-object-model.md",
"type": "file",
"path": "page-object-model.md",
"lines": 630
},
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 176
}
]
}
Related skills
FAQ
Which patterns are covered?
Domain-Driven Design, Clean Architecture, Hexagonal Architecture, and the Page Object Model for testing.
Why architecture for AI code generation?
Clear separation of concerns and consistent patterns reduce ambiguity so AI produces more correct, coherent code.