specwright/templates/skills/dev-team/backend/logic-implementing/SKILL.md
# [SKILL_NAME] - Backend Logic Implementation > **Role:** Backend Logic Implementer > **Domain:** Business Logic & Domain Models > **Created:** [CURRENT_DATE] ## Quick Reference <!-- This section is extracted by Orchestrator for task prompts (~50-100 lines) --> **When to use:** Service Objects, Business Logic, Domain Models, Use Cases, Validation **Key Patterns:** 1. **Service Object Pattern** - One class per use case - `call` method as entry point - Dependency injection for repos
npx skillsauth add michsindlinger/specwright specwright/templates/skills/dev-team/backend/logic-implementingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Role: Backend Logic Implementer Domain: Business Logic & Domain Models Created: [CURRENT_DATE]
When to use: Service Objects, Business Logic, Domain Models, Use Cases, Validation
Key Patterns:
Service Object Pattern
call method as entry pointValidation Pattern
call methodError Handling
Domain Model Rules
Quick Example (Rails):
class Users::Register
def initialize(user_repo: UserRepository.new)
@user_repo = user_repo
end
def call(params)
validate!(params)
user = @user_repo.create(params)
Result.success(user: user)
rescue ValidationError => e
Result.failure(errors: e.messages)
end
private
def validate!(params)
raise ValidationError, 'Email required' if params[:email].blank?
raise ValidationError, 'Email taken' if @user_repo.exists?(email: params[:email])
end
end
Anti-Patterns to Avoid:
Implement core business logic, domain models, and service orchestration for backend systems. Focus on clean architecture, SOLID principles, and maintainable business rules.
Use this skill for:
Do NOT use for:
# Service Object Pattern
class Users::CreateAccount
include ActiveModel::Validations
attr_reader :user, :errors
def initialize(params)
@params = params
@errors = ActiveModel::Errors.new(self)
end
def call
validate_params!
ActiveRecord::Base.transaction do
create_user
setup_profile
send_welcome_email
end
Success.new(user: @user)
rescue ValidationError => e
Failure.new(errors: e.errors)
end
private
def validate_params!
raise ValidationError, 'Email required' unless @params[:email].present?
raise ValidationError, 'Email taken' if User.exists?(email: @params[:email])
end
def create_user
@user = User.create!(
email: @params[:email],
password: @params[:password]
)
end
def setup_profile
@user.create_profile!(
name: @params[:name],
bio: @params[:bio]
)
end
def send_welcome_email
UserMailer.welcome(@user).deliver_later
end
end
# Value Object Pattern
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
@amount = BigDecimal(amount.to_s)
@currency = currency
end
def +(other)
raise CurrencyMismatch unless currency == other.currency
Money.new(amount + other.amount, currency)
end
def *(multiplier)
Money.new(amount * multiplier, currency)
end
def to_s
"#{currency} #{amount.round(2)}"
end
def <=>(other)
raise CurrencyMismatch unless currency == other.currency
amount <=> other.amount
end
end
# Policy Object Pattern
class OrderRefundPolicy
def initialize(order)
@order = order
end
def refundable?
return false if @order.refunded?
return false if days_since_purchase > 30
return false if @order.digital_goods? && @order.accessed?
true
end
def refund_amount
return Money.new(0) unless refundable?
if days_since_purchase <= 7
@order.total
elsif days_since_purchase <= 14
@order.total * 0.75
else
@order.total * 0.50
end
end
private
def days_since_purchase
(Time.current - @order.purchased_at) / 1.day
end
end
# Domain Event Pattern
class Order < ApplicationRecord
include ActiveModel::Dirty
after_commit :publish_status_change, if: :saved_change_to_status?
private
def publish_status_change
event = OrderStatusChanged.new(
order_id: id,
old_status: status_before_last_save,
new_status: status,
changed_at: updated_at
)
EventBus.publish(event)
end
end
// Service Class Pattern
interface CreateAccountParams {
email: string;
password: string;
name: string;
bio?: string;
}
interface CreateAccountResult {
success: boolean;
user?: User;
errors?: ValidationError[];
}
class CreateAccountService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async execute(params: CreateAccountParams): Promise<CreateAccountResult> {
// Validate input
const validationErrors = this.validate(params);
if (validationErrors.length > 0) {
return { success: false, errors: validationErrors };
}
// Check uniqueness
const existingUser = await this.userRepository.findByEmail(params.email);
if (existingUser) {
return {
success: false,
errors: [{ field: 'email', message: 'Email already taken' }]
};
}
// Execute in transaction
const user = await this.userRepository.transaction(async (tx) => {
const user = await this.userRepository.create({
email: params.email,
password: await this.hashPassword(params.password)
}, tx);
await this.userRepository.createProfile({
userId: user.id,
name: params.name,
bio: params.bio
}, tx);
return user;
});
// Background job
await this.emailService.sendWelcome(user.email);
return { success: true, user };
}
private validate(params: CreateAccountParams): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.email || !this.isValidEmail(params.email)) {
errors.push({ field: 'email', message: 'Invalid email format' });
}
if (!params.password || params.password.length < 8) {
errors.push({ field: 'password', message: 'Password must be at least 8 characters' });
}
return errors;
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
private async hashPassword(password: string): Promise<string> {
// Implementation
return 'hashed';
}
}
// Value Object Pattern
class Money {
private constructor(
public readonly amount: number,
public readonly currency: string = 'USD'
) {
if (amount < 0) {
throw new Error('Amount cannot be negative');
}
}
static create(amount: number, currency = 'USD'): Money {
return new Money(amount, currency);
}
add(other: Money): Money {
this.ensureSameCurrency(other);
return new Money(this.amount + other.amount, this.currency);
}
multiply(multiplier: number): Money {
return new Money(this.amount * multiplier, this.currency);
}
toString(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
private ensureSameCurrency(other: Money): void {
if (this.currency !== other.currency) {
throw new Error('Currency mismatch');
}
}
}
// Domain Event Pattern
interface DomainEvent {
occurredAt: Date;
eventType: string;
}
class OrderStatusChanged implements DomainEvent {
public readonly occurredAt = new Date();
public readonly eventType = 'OrderStatusChanged';
constructor(
public readonly orderId: string,
public readonly oldStatus: string,
public readonly newStatus: string
) {}
}
class Order {
private domainEvents: DomainEvent[] = [];
constructor(
public id: string,
public status: string
) {}
changeStatus(newStatus: string): void {
const oldStatus = this.status;
this.status = newStatus;
this.domainEvents.push(
new OrderStatusChanged(this.id, oldStatus, newStatus)
);
}
getDomainEvents(): DomainEvent[] {
return [...this.domainEvents];
}
clearDomainEvents(): void {
this.domainEvents = [];
}
}
# Service Layer Pattern
from dataclasses import dataclass
from typing import Optional
from django.db import transaction
@dataclass
class CreateAccountResult:
success: bool
user: Optional['User'] = None
errors: Optional[dict] = None
class CreateAccountService:
def __init__(self, user_repository, email_service):
self.user_repository = user_repository
self.email_service = email_service
def execute(self, email: str, password: str, name: str, bio: str = None) -> CreateAccountResult:
# Validate
errors = self._validate(email, password)
if errors:
return CreateAccountResult(success=False, errors=errors)
# Check uniqueness
if self.user_repository.exists_by_email(email):
return CreateAccountResult(
success=False,
errors={'email': 'Email already taken'}
)
# Execute in transaction
try:
with transaction.atomic():
user = self.user_repository.create(
email=email,
password=self._hash_password(password)
)
self.user_repository.create_profile(
user_id=user.id,
name=name,
bio=bio
)
# Background task
self.email_service.send_welcome.delay(user.email)
return CreateAccountResult(success=True, user=user)
except Exception as e:
return CreateAccountResult(
success=False,
errors={'general': str(e)}
)
def _validate(self, email: str, password: str) -> Optional[dict]:
errors = {}
if not email or '@' not in email:
errors['email'] = 'Invalid email format'
if not password or len(password) < 8:
errors['password'] = 'Password must be at least 8 characters'
return errors if errors else None
def _hash_password(self, password: str) -> str:
# Implementation
return 'hashed'
# Value Object Pattern
from decimal import Decimal
class Money:
def __init__(self, amount: Decimal, currency: str = 'USD'):
if amount < 0:
raise ValueError('Amount cannot be negative')
self.amount = amount
self.currency = currency
def __add__(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError('Currency mismatch')
return Money(self.amount + other.amount, self.currency)
def __mul__(self, multiplier: float) -> 'Money':
return Money(self.amount * Decimal(str(multiplier)), self.currency)
def __str__(self) -> str:
return f'{self.currency} {self.amount:.2f}'
def __eq__(self, other: 'Money') -> bool:
return self.amount == other.amount and self.currency == other.currency
[MCP_TOOLS]
<!-- Populated during skill creation based on: 1. User's installed MCP servers 2. User's selection for this skill Recommended for this skill (examples): - None required for basic business logic implementation - Optional: Database inspection tools for debugging - Optional: Code analysis tools for complexity metrics Note: Skills work without MCP servers, but functionality may be limited --># RSpec example
RSpec.describe Users::CreateAccount do
describe '#call' do
let(:params) do
{
email: '[email protected]',
password: 'password123',
name: 'John Doe'
}
end
it 'creates a new user' do
result = described_class.new(params).call
expect(result).to be_success
expect(result.user).to be_persisted
expect(result.user.email).to eq('[email protected]')
end
it 'creates a profile for the user' do
result = described_class.new(params).call
expect(result.user.profile).to be_present
expect(result.user.profile.name).to eq('John Doe')
end
it 'sends welcome email' do
expect {
described_class.new(params).call
}.to have_enqueued_job(UserMailerJob)
end
context 'when email is already taken' do
before { create(:user, email: '[email protected]') }
it 'returns failure' do
result = described_class.new(params).call
expect(result).to be_failure
expect(result.errors).to include('Email taken')
end
end
end
end
// Jest example
describe('CreateAccountService', () => {
let service: CreateAccountService;
let userRepository: jest.Mocked<UserRepository>;
let emailService: jest.Mocked<EmailService>;
beforeEach(() => {
userRepository = {
findByEmail: jest.fn(),
create: jest.fn(),
createProfile: jest.fn(),
transaction: jest.fn((cb) => cb(null))
} as any;
emailService = {
sendWelcome: jest.fn()
} as any;
service = new CreateAccountService(userRepository, emailService);
});
describe('execute', () => {
const validParams = {
email: '[email protected]',
password: 'password123',
name: 'John Doe'
};
it('creates a new user account', async () => {
userRepository.findByEmail.mockResolvedValue(null);
userRepository.create.mockResolvedValue({ id: '1', email: validParams.email });
const result = await service.execute(validParams);
expect(result.success).toBe(true);
expect(result.user).toBeDefined();
expect(userRepository.create).toHaveBeenCalled();
});
it('sends welcome email', async () => {
userRepository.findByEmail.mockResolvedValue(null);
userRepository.create.mockResolvedValue({ id: '1', email: validParams.email });
await service.execute(validParams);
expect(emailService.sendWelcome).toHaveBeenCalledWith(validParams.email);
});
it('returns error when email is taken', async () => {
userRepository.findByEmail.mockResolvedValue({ id: '2', email: validParams.email });
const result = await service.execute(validParams);
expect(result.success).toBe(false);
expect(result.errors).toContainEqual({
field: 'email',
message: 'Email already taken'
});
});
});
});
Use Result/Either pattern for operation outcomes:
Encapsulate operations as objects:
Swap algorithms at runtime:
Model complex state transitions:
Remember: Business logic should be framework-agnostic, testable in isolation, and clearly express domain concepts. Focus on correctness, clarity, and maintainability over premature optimization.
tools
Session Handoff: Erstellt eine vollständige Zusammenfassung der aktuellen Session für einen sauberen Kontextwechsel. NUR bei explizitem Aufruf (/session-handoff). NICHT automatisch auslösen. Geeignet wenn der User die Session resetten will, den Kontext aufräumen will, oder bei ~120k Tokens angelangt ist.
development
Pre-Mortem Risk Analysis: Strukturierte Prospective-Hindsight-Übung um launch-blocking Risiken vor Commitment aufzudecken. Team stellt sich vor, das Produkt sei 14 Tage nach Launch gefloppt, und arbeitet rückwärts. Klassifiziert Risiken in Tigers (echt), Paper Tigers (hypothetisch), Elephants (unausgesprochen). Nutze diesen Skill vor Build-Commitment, bei zu hoher Stakeholder-Confidence, vor Major-Releases, oder wenn das Team vage Sorgen nicht artikulieren kann. Trigger: /pre-mortem, 'pre-mortem', 'risk analysis', 'was könnte schiefgehen', 'risiken vor launch'.
testing
Six-Sigma Atomicity Validator for create-spec stories
tools
UX pattern definition guidance for navigation, user flows, interactions, and accessibility