Loading learning content...
Code is read far more often than it's written. Studies suggest developers spend 10-20 times more time reading code than writing it. This reality has a profound implication: the visual layout of code—how we arrange elements within files—directly impacts productivity.
Code layout encompasses decisions like:
These might seem like minor aesthetic preferences, but they accumulate. A codebase with consistent, thoughtful layout is dramatically easier to navigate and understand. A codebase with inconsistent or poor layout creates constant friction—small annoyances that compound into significant cognitive overhead.
By the end of this page, you will understand the principles behind effective code layout, master the conventions for ordering class members, learn when and how to use whitespace for clarity, and develop habits that make code visually scannable and easy to navigate.
Layout isn't about making code 'pretty'—it's about making code scannable. Developers rarely read code line by line like a novel; they scan, looking for landmarks, patterns, and relevant sections. Good layout supports this scanning behavior.
❌ Inconsistent, hard-to-scan layout:
class OrderService {
private repo: OrderRepository;
public async getOrder(id: string) {
const order = await this.repo.find(id);
if (!order) { throw new NotFoundError(); }
return order;
}
constructor(repo: OrderRepository) {
this.repo = repo;
}
public async createOrder(data: CreateOrderData) {
const order = new Order(data);
await this.repo.save(order);
return order;
}
private validate(data: any): boolean {
return data.items?.length > 0;
}
}
No visual structure. Constructor in the middle. Private/public mixed.
✅ Clean, scannable layout:
class OrderService {
private repo: OrderRepository;
constructor(repo: OrderRepository) {
this.repo = repo;
}
// --- Public API ---
public async getOrder(id: string) {
const order = await this.repo.find(id);
if (!order) {
throw new NotFoundError();
}
return order;
}
public async createOrder(data: CreateOrderData) {
const order = new Order(data);
await this.repo.save(order);
return order;
}
// --- Private Helpers ---
private validate(data: any): boolean {
return data.items?.length > 0;
}
}
Clear sections. Predictable ordering. Easy to scan.
Robert C. Martin's 'newspaper metaphor': a file should read like a newspaper article. The most important, high-level information (public API) comes first—the headline. Details follow in descending order of importance. Readers can stop at any point and have a complete picture at that level of detail.
How you order members within a class significantly impacts readability. There are two dominant philosophies, each with merits:
| Approach | Order | Rationale |
|---|---|---|
| Visibility-first | Public → Protected → Private (within each: static → instance → constructor → methods → fields) | Most important (public API) visible first; implementation details hidden below |
| Member-type-first | Fields → Constructor → Methods (within each: static → instance → public → private) | Logical flow: initialization before behavior; state before operations |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
/** * OrderService - Handles order lifecycle operations * * Standard class structure: visibility-first ordering */class OrderService { // ========================================== // 1. STATIC MEMBERS (if any) // ========================================== private static readonly DEFAULT_PAGE_SIZE = 20; // ========================================== // 2. INSTANCE FIELDS // ========================================== private readonly repository: OrderRepository; private readonly eventBus: EventBus; private readonly logger: Logger; // ========================================== // 3. CONSTRUCTOR // ========================================== constructor( repository: OrderRepository, eventBus: EventBus, logger: Logger ) { this.repository = repository; this.eventBus = eventBus; this.logger = logger; } // ========================================== // 4. PUBLIC METHODS (the class's API) // ========================================== /** * Retrieves an order by ID * @throws NotFoundError if order doesn't exist */ public async getOrder(orderId: string): Promise<Order> { this.logger.debug(`Fetching order ${orderId}`); const order = await this.repository.findById(orderId); if (!order) { throw new NotFoundError(`Order ${orderId} not found`); } return order; } /** * Creates a new order from the given data */ public async createOrder(data: CreateOrderData): Promise<Order> { this.validateOrderData(data); const order = this.buildOrder(data); await this.repository.save(order); await this.eventBus.publish(new OrderCreatedEvent(order)); return order; } /** * Lists orders with pagination */ public async listOrders(options: ListOptions = {}): Promise<PaginatedResult<Order>> { const pageSize = options.pageSize ?? OrderService.DEFAULT_PAGE_SIZE; return this.repository.findAll({ ...options, pageSize }); } // ========================================== // 5. PRIVATE METHODS (implementation details) // ========================================== private validateOrderData(data: CreateOrderData): void { if (!data.items || data.items.length === 0) { throw new ValidationError('Order must have at least one item'); } if (!data.customerId) { throw new ValidationError('Customer ID is required'); } } private buildOrder(data: CreateOrderData): Order { return new Order({ id: generateId(), customerId: data.customerId, items: data.items, status: OrderStatus.PENDING, createdAt: new Date(), }); }}Both ordering approaches are valid. What matters is consistency: all classes in a project should follow the same ordering convention. Many teams enforce this with linters (ESLint's member-ordering rule, Checkstyle's DeclarationOrder, etc.).
Blank lines are paragraph breaks in code. They separate logical units and signal transitions in thought. Used well, they make code far more scannable. Used poorly (or not at all), code becomes a wall of text.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// ❌ No vertical spacing - hard to readasync function processOrder(orderId: string): Promise<void> { const order = await this.orderRepo.findById(orderId); if (!order) { throw new NotFoundError('Order not found'); } const customer = await this.customerRepo.findById(order.customerId); if (!customer) { throw new NotFoundError('Customer not found'); } const paymentResult = await this.paymentService.charge(customer, order.total); if (!paymentResult.success) { await this.orderRepo.updateStatus(orderId, 'PAYMENT_FAILED'); throw new PaymentError(paymentResult.message); } await this.orderRepo.updateStatus(orderId, 'PAID'); await this.notificationService.sendConfirmation(customer.email, order); await this.inventoryService.reserve(order.items);} // ✅ Logical grouping with blank linesasync function processOrder(orderId: string): Promise<void> { // Fetch required entities const order = await this.orderRepo.findById(orderId); if (!order) { throw new NotFoundError('Order not found'); } const customer = await this.customerRepo.findById(order.customerId); if (!customer) { throw new NotFoundError('Customer not found'); } // Process payment const paymentResult = await this.paymentService.charge(customer, order.total); if (!paymentResult.success) { await this.orderRepo.updateStatus(orderId, 'PAYMENT_FAILED'); throw new PaymentError(paymentResult.message); } // Update state and notify await this.orderRepo.updateStatus(orderId, 'PAID'); await this.notificationService.sendConfirmation(customer.email, order); await this.inventoryService.reserve(order.items);}The paragraph principle:
Think of blank lines as paragraph breaks in prose. Just as a paragraph groups related sentences, a code 'paragraph' groups related statements. When the thought changes—new validation, new operation, new phase—start a new paragraph.
Signs you need a blank line:
While blank lines are valuable, too many fragment the code. Multiple consecutive blank lines or blank lines between every statement make code feel disconnected and wasteful of screen space. One blank line is almost always sufficient.
Horizontal layout involves indentation, line length, alignment, and spacing within lines. These choices affect how easily the eye can follow code structure.
| Aspect | Guideline | Rationale |
|---|---|---|
| Line length | 80-120 characters max | Prevents horizontal scrolling; multiple files side-by-side |
| Indentation | 2 or 4 spaces (consistent) | Shows nesting hierarchy; tabs vs spaces - pick one |
| Operator spacing | Space around binary operators | a + b not a+b; improves readability |
| Comma spacing | Space after commas | fn(a, b, c) not fn(a,b,c) |
| Alignment | Avoid excessive alignment | Aligned code requires maintenance; changes cascade |
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// ❌ Line too long - requires horizontal scrollingconst result = await this.orderService.createOrder({ customerId: customer.id, items: cart.items, shippingAddress: customer.defaultAddress, billingAddress: customer.billingAddress, paymentMethod: customer.preferredPayment }); // ✅ Broken across lines for readabilityconst result = await this.orderService.createOrder({ customerId: customer.id, items: cart.items, shippingAddress: customer.defaultAddress, billingAddress: customer.billingAddress, paymentMethod: customer.preferredPayment,}); // ❌ Over-aligned - hard to maintainconst name = 'John';const email = 'john@example.com';const phoneNumber = '555-1234';const address = '123 Main St'; // ✅ Simple, consistent formattingconst name = 'John';const email = 'john@example.com';const phoneNumber = '555-1234';const address = '123 Main St'; // ❌ Cramped expressionsconst total=price*quantity+tax-discount;if(order.status==='pending'&&order.items.length>0){ // ✅ Spaced for clarityconst total = price * quantity + tax - discount;if (order.status === 'pending' && order.items.length > 0) { // Long conditions - break for readability// ❌ Hard to parseif (user.isActive && user.hasPermission('admin') && user.department === 'engineering' && !user.isOnLeave) { // ✅ Each condition on its own lineif ( user.isActive && user.hasPermission('admin') && user.department === 'engineering' && !user.isOnLeave) { // ...}Automatic formatters (Prettier, Black, gofmt, rustfmt) eliminate formatting debates and ensure consistency. Configure them in your project and run on save. Never commit unformatted code. The formatter's style becomes 'correct' by definition.
A well-organized file follows a predictable structure from top to bottom. Readers should be able to navigate based on expectations about where things are.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
// =====================================================// 1. FILE HEADER (optional - auto-generated license, etc.)// =====================================================/** * OrderService.ts * * Handles order lifecycle including creation, processing, and fulfillment. */ // =====================================================// 2. IMPORTS - organized by category// ===================================================== // External dependencies (node_modules)import { Injectable } from '@nestjs/common';import { v4 as uuid } from 'uuid'; // Internal absolute imports (project modules)import { Logger } from '@/infrastructure/logging';import { EventBus } from '@/infrastructure/events'; // Relative imports (same module/feature)import { Order, OrderStatus } from './Order';import { OrderRepository } from './OrderRepository';import { CreateOrderData, OrderDTO } from './order.types'; // =====================================================// 3. TYPE DEFINITIONS (if local to this file)// ===================================================== interface ProcessingResult { success: boolean; orderId: string; message?: string;} // =====================================================// 4. CONSTANTS (file-level)// ===================================================== const MAX_ITEMS_PER_ORDER = 100;const DEFAULT_PROCESSING_TIMEOUT_MS = 30000; // =====================================================// 5. MAIN CLASS/FUNCTION EXPORTS// ===================================================== @Injectable()export class OrderService { // ... class implementation} // =====================================================// 6. HELPER FUNCTIONS (if not in separate file)// ===================================================== function validateItemCount(items: OrderItem[]): void { if (items.length > MAX_ITEMS_PER_ORDER) { throw new ValidationError(`Cannot exceed ${MAX_ITEMS_PER_ORDER} items`); }} // =====================================================// 7. DEFAULT EXPORT (if applicable)// ===================================================== export default OrderService;import * as foo obscures what's actually used; makes tree-shaking less effective.import { A, B, C } from './index' can cause circular dependency issues and bundle bloat.While not universal, many teams adopt a 'one class/function per file' rule. This simplifies file naming (file name matches export), makes responsibility clear, and prevents files from growing too large. Helper functions tightly coupled to the main export can stay in the same file.
Within functions, layout affects how easily readers can follow the logic. The key principle: keep related code together, and separate distinct phases.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Well-structured function layoutasync function processOrderSubmission( orderId: string, paymentDetails: PaymentDetails): Promise<OrderConfirmation> { // Phase 1: Validation and early returns (guard clauses) const order = await this.orderRepo.findById(orderId); if (!order) { throw new NotFoundError(`Order ${orderId} not found`); } if (order.status !== OrderStatus.PENDING) { throw new InvalidStateError(`Cannot process order in ${order.status} state`); } // Phase 2: Gather required data const customer = await this.customerRepo.findById(order.customerId); const inventory = await this.inventoryService.checkAvailability(order.items); if (!inventory.allAvailable) { throw new InsufficientInventoryError(inventory.unavailableItems); } // Phase 3: Core business logic const paymentResult = await this.paymentService.processPayment({ customerId: customer.id, amount: order.total, ...paymentDetails, }); if (!paymentResult.success) { await this.handlePaymentFailure(order, paymentResult); throw new PaymentFailedError(paymentResult.errorMessage); } // Phase 4: Update state order.markAsPaid(paymentResult.transactionId); await this.orderRepo.save(order); // Phase 5: Side effects (notifications, events) await this.inventoryService.reserveItems(order.items); await this.eventBus.publish(new OrderPaidEvent(order)); await this.notificationService.sendConfirmation(customer.email, order); // Phase 6: Return result return this.buildOrderConfirmation(order, paymentResult);}If your function has clear phases (as in the example above), consider extracting each phase to a separate function. This makes the high-level flow explicit: validate(), gatherData(), processPayment(), updateState(), notifyParties(). The main function becomes a readable orchestration.
Comments are part of code layout. When and how to use them significantly affects readability. The goal: comments should add value, not restate the obvious.
| Comment Type | Purpose | Example |
|---|---|---|
| JSDoc/Javadoc | Public API documentation | Parameters, return values, exceptions |
| Why comments | Explain non-obvious decisions | Why this algorithm, not another |
| Warning comments | Alert to potential issues | Performance implications, side effects |
| TODO/FIXME | Track technical debt | Known issues, planned improvements |
| Section markers | Delineate major sections | // --- Public API --- |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// ❌ USELESS COMMENTS - Restate the obvious// Increment countercounter++; // Check if user is activeif (user.isActive) { } // Loop through itemsfor (const item of items) { } // Constructorconstructor() { } // ✅ VALUABLE COMMENTS - Add information not in the code /** * Calculates compound interest using the standard formula. * * @param principal - Initial investment amount * @param rate - Annual interest rate as decimal (e.g., 0.05 for 5%) * @param timesCompounded - Number of times interest compounds per year * @param years - Investment duration in years * @returns The final amount including principal and interest * * @example * // $1000 at 5% compounded monthly for 10 years * calculateCompoundInterest(1000, 0.05, 12, 10) // Returns $1647.01 */function calculateCompoundInterest( principal: number, rate: number, timesCompounded: number, years: number): number { // Standard compound interest formula: A = P(1 + r/n)^(nt) return principal * Math.pow(1 + rate / timesCompounded, timesCompounded * years);} // Why we use binary search instead of hash lookup:// The dataset is already sorted from the upstream service, and we need// range queries that hash maps can't efficiently support. Binary search// gives us O(log n) lookups with O(1) range scan initiation.const index = binarySearch(sortedUsers, targetId); // WARNING: This method has O(n²) complexity. For collections over 1000 items,// use bulkProcess() instead which batches into O(n log n) operations.function processItems(items: Item[]): void { // ...} // TODO(ticket-1234): Replace with proper retry mechanism once circuit breaker is implementedawait sleep(1000);await retryOperation();If you find yourself writing a comment to explain what code does, consider whether better naming or function extraction would eliminate the need. Comments explaining what often indicate the code isn't self-explanatory. Comments explaining why are usually valuable.
Error handling code can dominate a function's layout if not organized carefully. The goal is to keep the happy path prominent while handling errors cleanly.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// ❌ Error handling obscures happy pathasync function fetchUserData(userId: string): Promise<UserData> { try { const user = await this.userRepo.findById(userId); if (!user) { throw new NotFoundError('User not found'); } try { const preferences = await this.prefsRepo.findByUserId(userId); if (!preferences) { throw new NotFoundError('Preferences not found'); } try { const activity = await this.activityRepo.findRecent(userId); return { user, preferences, activity: activity || [], }; } catch (activityError) { this.logger.warn('Failed to fetch activity', activityError); return { user, preferences, activity: [] }; } } catch (prefError) { this.logger.warn('Failed to fetch preferences', prefError); return { user, preferences: DEFAULT_PREFERENCES, activity: [] }; } } catch (userError) { this.logger.error('Failed to fetch user', userError); throw userError; }} // ✅ Guard clauses + flat structure + focused error handlingasync function fetchUserData(userId: string): Promise<UserData> { // Fetch required data (throws if user not found) const user = await this.fetchUserOrThrow(userId); // Fetch optional data with fallbacks const preferences = await this.fetchPreferencesWithFallback(userId); const activity = await this.fetchActivityWithFallback(userId); return { user, preferences, activity };} private async fetchUserOrThrow(userId: string): Promise<User> { const user = await this.userRepo.findById(userId); if (!user) { throw new NotFoundError(`User ${userId} not found`); } return user;} private async fetchPreferencesWithFallback(userId: string): Promise<Preferences> { try { const prefs = await this.prefsRepo.findByUserId(userId); return prefs ?? DEFAULT_PREFERENCES; } catch (error) { this.logger.warn(`Failed to fetch preferences for ${userId}`, error); return DEFAULT_PREFERENCES; }} private async fetchActivityWithFallback(userId: string): Promise<Activity[]> { try { return await this.activityRepo.findRecent(userId) ?? []; } catch (error) { this.logger.warn(`Failed to fetch activity for ${userId}`, error); return []; }}Manual formatting is error-prone and contentious. Modern development relies on automated tools to eliminate formatting debates and ensure consistency.
| Language | Formatter | Linter | Key Configs |
|---|---|---|---|
| TypeScript/JS | Prettier | ESLint | .prettierrc, .eslintrc |
| Python | Black, autopep8 | Pylint, Flake8, Ruff | pyproject.toml |
| Java | google-java-format | Checkstyle, SpotBugs | checkstyle.xml |
| Go | gofmt (built-in) | golint, staticcheck | N/A (standard format) |
| Rust | rustfmt (built-in) | Clippy | rustfmt.toml |
| C# | dotnet format | StyleCop, Roslyn analyzers | .editorconfig |
123456789101112
// .prettierrc{ "semi": true, "singleQuote": true, "tabWidth": 4, "useTabs": false, "trailingComma": "es5", "printWidth": 100, "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf"}prettier --check exits non-zero for unformatted code.Formatting debates (tabs vs. spaces, 2 vs. 4 indent, etc.) waste energy. Pick a tool (Prettier, Black, gofmt), adopt its defaults, and move on. The best formatting style is the one the team stops thinking about because tools handle it automatically.
We've comprehensively covered code layout. Let's consolidate the key principles:
Code layout checklist:
Module Complete:
With this page, we've completed Module 8: Code Organization & Structure. You now have a comprehensive understanding of:
Congratulations! You've mastered Code Organization & Structure. These skills apply to every project you work on—they're the foundation of professional code that teams can maintain and evolve. Well-organized code isn't just aesthetically pleasing; it's a competitive advantage that compounds over the life of every project.