Skip to content

Abstractness

Abstractness in Leadline Architecture Design

Core Principle

Abstractness is about creating the right level of abstraction in your code to hide implementation details while exposing clean, simple interfaces. It enables flexibility, testability, and maintainability through proper decoupling.

What is Abstractness?

Abstractness refers to the practice of hiding complex implementation details behind simpler, more general interfaces. It allows you to work with concepts at a higher level without being concerned with the specific details of how things work underneath.

Levels of Abstraction

Low-Level Abstraction

Direct hardware interaction, memory management, system calls.

Mid-Level Abstraction

Data structures, algorithms, libraries, frameworks.

High-Level Abstraction

Business logic, domain models, application services.

Domain Abstraction

Business concepts, workflows, user interactions.

Benefits of Proper Abstraction

1. Flexibility and Extensibility

Abstractions allow you to change implementations without affecting the clients:

// ❌ Tightly coupled to specific implementation
class OrderService {
saveOrder(order) {
// Directly coupled to MySQL database
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'orders'
});
connection.execute(
'INSERT INTO orders (id, customer_id, total) VALUES (?, ?, ?)',
[order.id, order.customerId, order.total]
);
}
}

2. Testability

Abstractions make unit testing easier through dependency injection:

// Easy to test with mock implementations
describe('OrderService', () => {
let orderService: OrderService;
let mockRepository: jest.Mocked<OrderRepository>;
beforeEach(() => {
mockRepository = {
save: jest.fn(),
findById: jest.fn(),
findByCustomerId: jest.fn()
};
orderService = new OrderService(mockRepository);
});
it('should save order successfully', async () => {
const order = new Order('123', 'customer-1', 100);
await orderService.saveOrder(order);
expect(mockRepository.save).toHaveBeenCalledWith(order);
});
});

Abstraction Patterns

1. Repository Pattern

Abstracts data access logic:

interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
class DatabaseUserRepository implements UserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
const result = await this.db.query("SELECT * FROM users WHERE id = ?", [
id,
]);
return result.length > 0 ? this.mapToUser(result[0]) : null;
}
async save(user: User): Promise<void> {
await this.db.query(
"INSERT INTO users (id, email, name) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE email = VALUES(email), name = VALUES(name)",
[user.id, user.email, user.name],
);
}
private mapToUser(row: any): User {
return new User(row.id, row.email, row.name);
}
}

2. Factory Pattern

Abstracts object creation:

class NotificationFactory {
static create(type, config) {
const creators = {
email: () => new EmailNotification(config),
sms: () => new SMSNotification(config),
push: () => new PushNotification(config),
slack: () => new SlackNotification(config),
};
const creator = creators[type];
if (!creator) {
throw new Error(`Unknown notification type: ${type}`);
}
return creator();
}
}
// Usage - client doesn't need to know about specific implementations
const emailNotifier = NotificationFactory.create("email", {
smtpServer: "smtp.example.com",
});
const smsNotifier = NotificationFactory.create("sms", {
apiKey: "twilio-key",
});

3. Strategy Pattern

Abstracts algorithmic behavior:

interface PaymentStrategy {
processPayment(amount: number, details: any): Promise<PaymentResult>;
validatePaymentDetails(details: any): boolean;
}
class CreditCardStrategy implements PaymentStrategy {
async processPayment(
amount: number,
details: CreditCardDetails,
): Promise<PaymentResult> {
// Credit card processing logic
return { success: true, transactionId: `cc_${Date.now()}` };
}
validatePaymentDetails(details: CreditCardDetails): boolean {
return details.cardNumber && details.expiryDate && details.cvv;
}
}
class PayPalStrategy implements PaymentStrategy {
async processPayment(
amount: number,
details: PayPalDetails,
): Promise<PaymentResult> {
// PayPal processing logic
return { success: true, transactionId: `pp_${Date.now()}` };
}
validatePaymentDetails(details: PayPalDetails): boolean {
return details.email && details.password;
}
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
async process(amount: number, details: any): Promise<PaymentResult> {
if (!this.strategy.validatePaymentDetails(details)) {
throw new Error("Invalid payment details");
}
return await this.strategy.processPayment(amount, details);
}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
}

Domain-Driven Design Abstractions

Value Objects

Encapsulate business concepts with specific behaviors:

class Money {
constructor(
private readonly amount: number,
private readonly currency: string,
) {
if (amount < 0) {
throw new Error("Amount cannot be negative");
}
if (!currency || currency.length !== 3) {
throw new Error("Currency must be a valid 3-letter code");
}
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("Cannot add money with different currencies");
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return `${this.amount} ${this.currency}`;
}
}
// Usage
const price = new Money(100, "USD");
const tax = price.multiply(0.1);
const total = price.add(tax);

Entities and Aggregates

Model business concepts with identity and lifecycle:

class Order {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.DRAFT;
constructor(
private readonly id: OrderId,
private readonly customerId: CustomerId,
private createdAt: Date = new Date(),
) {}
addItem(product: Product, quantity: number): void {
if (this.status !== OrderStatus.DRAFT) {
throw new Error("Cannot modify confirmed order");
}
const existingItem = this.items.find((item) =>
item.productId.equals(product.id),
);
if (existingItem) {
existingItem.increaseQuantity(quantity);
} else {
this.items.push(new OrderItem(product.id, quantity, product.price));
}
}
confirm(): void {
if (this.items.length === 0) {
throw new Error("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
calculateTotal(): Money {
return this.items
.map((item) => item.getSubtotal())
.reduce((total, subtotal) => total.add(subtotal), new Money(0, "USD"));
}
}

API Design and Abstraction

1. Clean API Interfaces

Design APIs that hide complexity:

// ❌ Leaky abstraction - exposes implementation details
interface UserService {
getUserFromDatabase(sqlQuery: string): Promise<User>;
cacheUser(user: User, cacheKey: string, ttl: number): void;
invalidateUserCache(pattern: string): void;
}
// ✅ Clean abstraction - focuses on business operations
interface UserService {
findUserById(id: string): Promise<User | null>;
findUserByEmail(email: string): Promise<User | null>;
createUser(userData: CreateUserRequest): Promise<User>;
updateUser(id: string, updates: UpdateUserRequest): Promise<User>;
deleteUser(id: string): Promise<void>;
}

2. Fluent Interfaces

Create expressive, readable APIs:

class QueryBuilder {
constructor() {
this.query = {
select: [],
from: "",
where: [],
orderBy: [],
limit: null,
};
}
select(...fields) {
this.query.select.push(...fields);
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field, direction = "ASC") {
this.query.orderBy.push(`${field} ${direction}`);
return this;
}
limit(count) {
this.query.limit = count;
return this;
}
build() {
// Convert to SQL string
let sql = `SELECT ${this.query.select.join(", ")} FROM ${this.query.from}`;
if (this.query.where.length > 0) {
sql += ` WHERE ${this.query.where.join(" AND ")}`;
}
if (this.query.orderBy.length > 0) {
sql += ` ORDER BY ${this.query.orderBy.join(", ")}`;
}
if (this.query.limit) {
sql += ` LIMIT ${this.query.limit}`;
}
return sql;
}
}
// Usage - readable and expressive
const query = new QueryBuilder()
.select("id", "name", "email")
.from("users")
.where("active = 1")
.where('created_at > "2023-01-01"')
.orderBy("name")
.limit(10)
.build();

Guidelines for Effective Abstraction

  1. Start Concrete, Then Abstract

    • Build working solutions first
    • Identify patterns and commonalities
    • Extract abstractions when you see repetition
  2. Choose the Right Level

    • Not too specific (inflexible)
    • Not too general (complex)
    • Focus on the domain concepts
  3. Maintain Clear Boundaries

    • Each abstraction should have a single responsibility
    • Avoid leaky abstractions that expose implementation details
  4. Document Intent

    • Explain why the abstraction exists
    • Provide usage examples
    • Document assumptions and constraints

Common Anti-Patterns

  • God Interface: Interfaces that try to do everything
  • Leaky Abstraction: Implementation details bleeding through the interface
  • Premature Abstraction: Creating abstractions before understanding the real needs
  • Abstract Everything: Over-engineering simple operations

The key to successful abstractness is finding the right balance between flexibility and simplicity, always keeping the domain and user needs at the center of design decisions.