Low-Level Abstraction
Direct hardware interaction, memory management, system calls.
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.
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.
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.
Abstractions allow you to change implementations without affecting the clients:
// ❌ Tightly coupled to specific implementationclass 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] ); }}// ✅ Abstracted through interfaceinterface 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 hereawait this.orderRepository.save(order);}}
// Different implementations can be swapped easilyclass MySQLOrderRepository implements OrderRepository {async save(order: Order): Promise<void> {// MySQL implementation}}
class MongoOrderRepository implements OrderRepository {async save(order: Order): Promise<void> {// MongoDB implementation}}Abstractions make unit testing easier through dependency injection:
// Easy to test with mock implementationsdescribe('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); });});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); }}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 implementationsconst emailNotifier = NotificationFactory.create("email", { smtpServer: "smtp.example.com",});const smsNotifier = NotificationFactory.create("sms", { apiKey: "twilio-key",});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; }}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}`; }}
// Usageconst price = new Money(100, "USD");const tax = price.multiply(0.1);const total = price.add(tax);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")); }}Design APIs that hide complexity:
// ❌ Leaky abstraction - exposes implementation detailsinterface UserService { getUserFromDatabase(sqlQuery: string): Promise<User>; cacheUser(user: User, cacheKey: string, ttl: number): void; invalidateUserCache(pattern: string): void;}
// ✅ Clean abstraction - focuses on business operationsinterface 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>;}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 expressiveconst query = new QueryBuilder() .select("id", "name", "email") .from("users") .where("active = 1") .where('created_at > "2023-01-01"') .orderBy("name") .limit(10) .build();Start Concrete, Then Abstract
Choose the Right Level
Maintain Clear Boundaries
Document Intent
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.