Abstractness
Creating flexible architectures through proper abstraction and decoupling
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.
"The art of programming is the art of organizing complexity, of mastering multitude and avoiding its bastard chaos as effectively as possible." - Edsger Dijkstra
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
Flexibility and Extensibility
Abstractions allow you to change implementations without affecting the clients:
Problem
// ❌ 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,
]);
}
}Solution
// ✅ Abstracted through interface
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
findByCustomerId(customerId: string): Promise<Order[]>;
}
class OrderService {
constructor(private orderRepository: OrderRepository) {}
async saveOrder(order: Order): Promise<void> {
// Business logic here
await this.orderRepository.save(order);
}
}
// Different implementations can be swapped easily
class MySQLOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// MySQL implementation
}
}
class MongoOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// MongoDB implementation
}
}
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
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);
}
}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",
});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
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>;
}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
-
Start Concrete, Then Abstract
- Build working solutions first
- Identify patterns and commonalities
- Extract abstractions when you see repetition
-
Choose the Right Level
- Not too specific (inflexible)
- Not too general (complex)
- Focus on the domain concepts
-
Maintain Clear Boundaries
- Each abstraction should have a single responsibility
- Avoid leaky abstractions that expose implementation details
-
Document Intent
- Explain why the abstraction exists
- Provide usage examples
- Document assumptions and constraints
Avoid Over-Abstraction: Creating too many layers of abstraction can make code harder to understand and debug. Apply the principle of "you aren't gonna need it" (YAGNI) - don't abstract until you have a clear need.
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.
