Loading learning content...
Understanding the theoretical distinction between high-level and low-level modules is one thing; identifying them in a real codebase is another challenge entirely. Real-world code doesn't come with labels saying "high-level policy" or "low-level mechanism." It's often a mix of concerns, evolved organically over years, with boundaries that have blurred through maintenance and feature pressure.
This page provides systematic techniques for analyzing code and correctly classifying its components. These skills are essential for applying the Dependency Inversion Principle in practice — you can't fix dependency direction if you can't determine which way the dependencies should flow.
By the end of this page, you will be able to systematically analyze any module to determine its level, identify mixed-level code that violates proper architectural boundaries, apply multiple classification heuristics to ambiguous cases, and recognize the common patterns of level confusion in real codebases.
Rather than relying on gut feeling, we can apply a systematic framework for classifying code. This framework consists of multiple independent tests; a module's level is determined by the aggregate results across all tests.
| Test | High-Level Answer | Low-Level Answer |
|---|---|---|
| Business Relevance Test: Would a business stakeholder understand and care about this code's purpose? | Yes — describes business concepts | No — describes technical mechanisms |
| Technology Independence Test: Could this code work across different technology stacks unchanged? | Yes — pure logic, no tech dependencies | No — tied to specific technologies |
| Change Reason Test: What would cause this code to change? | Business requirement changes | Technology updates, performance tuning, scaling |
| Replaceability Test: Could you substitute a different implementation without changing clients? | N/A — this IS the logic | Yes — multiple implementations possible |
| Testing Strategy Test: How would you test this code in isolation? | Simple unit tests with plain objects | Requires mocks, stubs, or test infrastructure |
For any module, run through all five tests. If most answers point to 'high-level,' treat it as high-level. If most point to 'low-level,' treat it as low-level. If answers are mixed, the module likely has mixed concerns that should be separated.
The most intuitive test is business relevance: does this code express concepts that matter to the business? Let's apply this systematically.
For any piece of code, try to explain what it does using only business language. If you succeed, it's high-level. If you can't avoid technical terms, it's low-level.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// EXAMPLE 1: Clearly High-Levelclass LoanApprovalPolicy { evaluateApplication(application: LoanApplication): ApprovalDecision { // Business explanation: "Approve loans based on credit score and // debt-to-income ratio per our lending guidelines" if (application.creditScore < 580) { return ApprovalDecision.denied('Credit score below minimum threshold'); } const dtiRatio = application.totalMonthlyDebt.divideBy(application.grossMonthlyIncome); if (dtiRatio > 0.43) { return ApprovalDecision.denied('Debt-to-income ratio exceeds 43%'); } const maxLoanAmount = this.calculateMaxLoanAmount(application); if (application.requestedAmount.isGreaterThan(maxLoanAmount)) { return ApprovalDecision.conditionalApproval( 'Approved for reduced amount', maxLoanAmount ); } return ApprovalDecision.approved(application.requestedAmount); }}// ✓ A loan officer would understand every line of this code // EXAMPLE 2: Clearly Low-Levelclass PostgresLoanRepository { async save(application: LoanApplication): Promise<void> { // Technical explanation: "Serialize to JSON, bind to SQL parameters, // execute INSERT with ON CONFLICT upsert, handle connection pooling" const client = await this.pool.connect(); try { await client.query( `INSERT INTO loan_applications (id, applicant_id, amount, term_months, status, created_at, data) VALUES ($1, $2, $3, $4, $5, NOW(), $6) ON CONFLICT (id) DO UPDATE SET status = $5, data = $6, updated_at = NOW()`, [ application.id, application.applicantId, application.amount.cents, application.termMonths, application.status, JSON.stringify(application.toJSON()), ] ); } finally { client.release(); } }}// ✗ A loan officer would be lost by the second line // EXAMPLE 3: Mixed-Level (Problem!)class LoanService { async submitApplication(applicationDTO: any): Promise<any> { // HIGH-LEVEL: Business validation if (applicationDTO.amount < 1000) { throw new Error('Minimum loan amount is $1000'); } // LOW-LEVEL: Database operations mixed with business logic const existing = await this.pool.query( 'SELECT * FROM loan_applications WHERE applicant_id = $1 AND status = $2', [applicationDTO.applicantId, 'PENDING'] ); // HIGH-LEVEL: Business rule if (existing.rows.length > 0) { throw new Error('Cannot submit while another application is pending'); } // LOW-LEVEL: HTTP call to external system const creditScore = await axios.get( `${this.creditApiUrl}/scores/${applicationDTO.ssn}`, { headers: { 'Authorization': `Bearer ${this.creditApiKey}` } } ); // HIGH-LEVEL: Business decision const decision = this.approvalPolicy.evaluate(/*...*/); // LOW-LEVEL: More database operations await this.pool.query('INSERT INTO loan_applications ...', [...]); return decision; }}// ⚠ This code has HIGH and LOW-level concerns interleaved — refactoring neededExample 3 shows the most common pattern in real codebases: high-level business logic interspersed with low-level infrastructure code. This makes the code hard to test (needs database and HTTP mocks), hard to understand (business rules hidden in technical noise), and hard to change (modifying business rules might break database code and vice versa).
One of the most reliable ways to identify levels is to analyze dependencies. What a module imports reveals its level more accurately than what it exports.
Examine every import statement in a module and classify each imported dependency:
| Import Category | Classification | Examples |
|---|---|---|
| Database Drivers/ORMs | Low-Level | pg, mysql2, mongoose, prisma, typeorm |
| HTTP/Network Libraries | Low-Level | axios, fetch, got, express, fastify |
| Cloud SDKs | Low-Level | @aws-sdk/*, @google-cloud/*, @azure/* |
| Serialization Libraries | Low-Level | protobuf, avro, msgpack |
| Domain Entities/Value Objects | High-Level | Order, Customer, Money, Address |
| Abstract Interfaces | High-Level (Abstraction) | OrderRepository, PaymentGateway, NotificationService |
| Domain Services/Policies | High-Level | PricingPolicy, FulfillmentService, ValidationRules |
| Standard Library/Language Features | Neutral | Date, Map, Promise, Array |
Create an "import profile" for each module by counting imports in each category:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// HIGH-LEVEL MODULE: LoanApprovalService// Import Profile: Domain-heavy, no infrastructure import { LoanApplication } from '../domain/loan-application'; // Domain Entityimport { ApplicantProfile } from '../domain/applicant-profile'; // Domain Entityimport { Money } from '../domain/value-objects/money'; // Value Objectimport { ApprovalDecision } from '../domain/approval-decision'; // Domain Entityimport { LoanPolicy } from '../policies/loan-policy'; // Domain Policyimport { CreditAssessment } from '../domain/credit-assessment'; // Domain Entityimport { ApplicationRepository } from './application-repository'; // Abstract Interface // Analysis:// - 6 domain/business imports// - 1 abstract interface// - 0 infrastructure imports// Verdict: CLEARLY HIGH-LEVEL ✓ // LOW-LEVEL MODULE: PostgresApplicationRepository// Import Profile: Infrastructure-heavy import { Pool, QueryResult } from 'pg'; // Database driverimport { RedisClient } from 'ioredis'; // Cache clientimport { Logger } from 'winston'; // Loggingimport { ApplicationRepository } from '../application-repository'; // Interface to implementimport { LoanApplication } from '../../domain/loan-application'; // Domain for mappingimport { Money } from '../../domain/value-objects/money'; // Domain for mapping // Analysis:// - 3 infrastructure imports// - 1 interface (to implement)// - 2 domain imports (for data mapping)// Verdict: CLEARLY LOW-LEVEL ✓ // MIXED-LEVEL MODULE (Problem!): LoanController// Import Profile: Mixed — both domain and infrastructure import { Request, Response } from 'express'; // Web framework (low)import { Pool } from 'pg'; // Database (low)import { LoanApplication } from '../domain/loan-application'; // Domain (high)import { LoanPolicy } from '../policies/loan-policy'; // Policy (high)import axios from 'axios'; // HTTP client (low)import { validate } from 'class-validator'; // Validation lib (low) // Analysis:// - 4 infrastructure imports// - 2 domain imports// Verdict: MIXED-LEVEL — Needs refactoring ⚠High-level modules should import primarily from domain packages and abstract interfaces. Low-level modules import from both infrastructure libraries (to do their job) and domain packages (to implement interfaces). If a module imports heavily from both infrastructure AND contains business logic — it's mixed-level and needs separation.
Another powerful technique is analyzing why code would change. This connects directly to the Single Responsibility Principle — different reasons for change indicate different levels of abstraction.
For any module, brainstorm realistic change scenarios and categorize them:
These changes indicate high-level concerns:
If a module would change because of scenarios like these, it contains high-level business logic.
How easily can you test code in isolation? This testability criterion is a powerful proxy for level classification.
For any module, imagine writing a comprehensive test. What would you need?
High-level modules are testable with simple unit tests:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
// HIGH-LEVEL: Easy to test — plain objects, no infrastructuredescribe('PricingPolicy', () => { const policy = new PricingPolicy(); test('applies member discount', () => { const context = { product: { basePrice: Money.of(100, 'USD') }, quantity: 1, customer: { isMember: () => true }, activePromotions: [], pricingDate: new Date(), }; const result = policy.calculatePrice(context); // No database, no HTTP, no mocks — just pure logic testing expect(result.finalPrice.amount).toBe(90); // 10% off expect(result.appliedDiscounts).toContainEqual( expect.objectContaining({ type: 'MEMBER_DISCOUNT' }) ); }); test('caps total discount at 30%', () => { const context = { product: { basePrice: Money.of(100, 'USD') }, quantity: 15, // triggers bulk discount customer: { isMember: () => true }, activePromotions: [ { discountPercent: 20, isActiveOn: () => true, appliesTo: () => true } ], pricingDate: new Date(), }; const result = policy.calculatePrice(context); // 10% member + 5% bulk + 20% promo = 35%, capped at 30% expect(result.finalPrice.amount).toBe(70); // Max 30% off });}); // LOW-LEVEL: Requires mocks or real infrastructure to testdescribe('PostgresOrderRepository', () => { let pool: MockPool; // ← Need a mock let repository: PostgresOrderRepository; beforeEach(() => { // ← Setup infrastructure mocks pool = createMockPool(); repository = new PostgresOrderRepository(pool); }); test('persists order', async () => { const order = createTestOrder(); await repository.save(order); // ← Verify SQL query was called with right params expect(pool.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO orders'), expect.arrayContaining([order.id]) ); });}); // INTEGRATION TEST: Sometimes you test low-level with real infrastructuredescribe('PostgresOrderRepository (integration)', () => { let pool: Pool; // ← Real database connection let repository: PostgresOrderRepository; beforeAll(async () => { // ← Need real database setup pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL }); await pool.query('DELETE FROM orders WHERE id LIKE $1', ['test-%']); }); afterAll(async () => { await pool.end(); }); test('round-trips order through database', async () => { // ← Test with real PostgreSQL const order = createTestOrder(); await repository.save(order); const retrieved = await repository.findById(order.id); expect(retrieved).toEqual(order); });});The amount of infrastructure required for testing reveals the module's level:
| Testing Requirement | Module Level | Example |
|---|---|---|
| Plain objects only | Pure High-Level | Domain entities, value objects, pure policies |
| Simple test doubles/stubs | High-Level with Dependencies | Services depending on repository interfaces |
| Complex mocks with behavior verification | Mixed-Level (problematic) | Code with embedded infrastructure |
| Real database/network required | Low-Level Infrastructure | Repository implementations, API clients |
| Full environment (containers, external services) | Integration/System Level | End-to-end workflows |
If testing your business rules requires setting up a database, mocking HTTP calls, or spinning up containers — your business rules are too entangled with infrastructure. High-level logic should be testable with nothing but in-memory objects and simple assertions.
Certain patterns of code organization consistently lead to level confusion. Recognizing these antipatterns helps you refactor toward proper level separation.
A "service" class that does everything: validation, business logic, database access, external API calls, and response formatting.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// ❌ ANTIPATTERN: Fat Service mixing all levelsclass OrderService { constructor( private pool: Pool, // Low-level direct dependency private stripeClient: Stripe, // Low-level direct dependency private emailClient: SendGridClient, // Low-level direct dependency ) {} async createOrder(dto: any): Promise<any> { // Validation (should be separate) if (!dto.items?.length) throw new Error('No items'); // Database read (low-level) const customer = await this.pool.query( 'SELECT * FROM customers WHERE id = $1', [dto.customerId] ); // Business logic (high-level) let total = dto.items.reduce((sum, item) => sum + item.price * item.qty, 0); if (customer.rows[0].tier === 'premium') { total *= 0.9; // 10% discount } // Payment processing (low-level API call) const charge = await this.stripeClient.charges.create({ amount: Math.round(total * 100), currency: 'usd', customer: customer.rows[0].stripe_customer_id, }); // Database write (low-level) await this.pool.query( 'INSERT INTO orders (customer_id, total, stripe_charge_id) VALUES ($1, $2, $3)', [dto.customerId, total, charge.id] ); // Email sending (low-level) await this.emailClient.send({ to: customer.rows[0].email, template: 'order-confirmation', data: { orderId: charge.id, total }, }); return { success: true, chargeId: charge.id }; }}This class is untestable without mocking three external systems. Business rules (the 10% premium discount) are buried in infrastructure code. Changing the email provider requires touching the same file as changing pricing logic. Every developer working on orders steps on each other.
Let's apply our classification techniques to a realistic codebase structure. Given a typical e-commerce application, we'll classify each component:
12345678910111213141516171819202122232425262728293031323334353637383940
src/├── domain/│ ├── entities/│ │ ├── Order.ts # HIGH-LEVEL: Pure business entity│ │ ├── Customer.ts # HIGH-LEVEL: Pure business entity│ │ └── Product.ts # HIGH-LEVEL: Pure business entity│ ├── value-objects/│ │ ├── Money.ts # HIGH-LEVEL: Immutable calculations│ │ ├── Address.ts # HIGH-LEVEL: Domain concept│ │ └── OrderId.ts # HIGH-LEVEL: Identity with rules│ └── services/│ ├── PricingService.ts # HIGH-LEVEL: Business calculations│ └── FulfillmentPolicy.ts # HIGH-LEVEL: Business rules│├── application/│ ├── use-cases/│ │ ├── CreateOrder.ts # HIGH-LEVEL: Application workflow│ │ ├── ProcessPayment.ts # HIGH-LEVEL: Application workflow│ │ └── ShipOrder.ts # HIGH-LEVEL: Application workflow│ └── interfaces/│ ├── OrderRepository.ts # HIGH-LEVEL (ABSTRACTION)│ ├── PaymentGateway.ts # HIGH-LEVEL (ABSTRACTION)│ └── ShippingProvider.ts # HIGH-LEVEL (ABSTRACTION)│├── infrastructure/│ ├── persistence/│ │ ├── PostgresOrderRepository.ts # LOW-LEVEL: Database impl│ │ ├── PostgresCustomerRepository.ts # LOW-LEVEL: Database impl│ │ └── database.ts # LOW-LEVEL: Connection setup│ ├── external/│ │ ├── StripePaymentGateway.ts # LOW-LEVEL: Stripe integration│ │ ├── ShippoProvider.ts # LOW-LEVEL: Shipping API│ │ └── SendGridEmailService.ts # LOW-LEVEL: Email API│ └── web/│ ├── OrderController.ts # LOW-LEVEL: HTTP handling│ ├── middleware/ # LOW-LEVEL: Express middleware│ └── routes.ts # LOW-LEVEL: URL mapping│└── config/ └── dependencies.ts # Composition root — wires it all together| Component | Level | Rationale |
|---|---|---|
domain/entities/* | High-Level | Pure business concepts, no infrastructure imports |
domain/value-objects/* | High-Level | Immutable domain primitives, pure calculations |
domain/services/* | High-Level | Business rules and policies, domain dependencies only |
application/use-cases/* | High-Level | Application workflows using domain and abstractions |
application/interfaces/* | Abstraction | Contracts defined by high-level needs, implemented by low-level |
infrastructure/persistence/* | Low-Level | Database-specific implementations, SQL, connection pooling |
infrastructure/external/* | Low-Level | Third-party API integrations, SDK usage |
infrastructure/web/* | Low-Level | HTTP handling, request/response formatting |
Notice how dependencies flow: Infrastructure components depend on abstractions defined in the application layer. Business logic in domain depends on nothing outside domain. This is DIP in action — high-level modules don't depend on low-level modules; both depend on abstractions.
Coming Up Next:
Now that you can identify high-level and low-level code, the final piece is understanding how to protect high-level modules from low-level changes. The next page covers the architectural techniques that insulate your business logic from infrastructure volatility.
You now have systematic techniques for classifying any code as high-level or low-level. These skills are essential for recognizing DIP violations and understanding which dependencies need to be inverted. Practice these techniques on your own codebase to see the patterns in action.