Loading learning content...
Every software system, regardless of its size or complexity, is a tapestry woven from distinct threads of functionality. A web application simultaneously handles user authentication, data validation, business logic, persistence, logging, error handling, and presentation. Each of these represents a different concern—a distinct area of responsibility within the system.
The ability to recognize and articulate these concerns is the first step toward building software that remains comprehensible and malleable throughout its lifecycle. Without this recognition, systems become tangled masses where every change requires understanding everything at once, and modifying one feature inadvertently breaks another.
By the end of this page, you will understand what constitutes a 'concern' in software design, develop techniques for identifying concerns in any system, and learn to distinguish between different types of concerns. This foundational skill enables every subsequent principle of separation—you cannot separate what you cannot see.
In software engineering, a concern is any piece of interest or focus in a program. More precisely, it is a distinct aspect of functionality that can be identified, discussed, and potentially isolated from other aspects. The term was popularized by Edsger W. Dijkstra in his seminal 1974 paper "On the role of scientific thought," where he introduced the concept of separation of concerns as a fundamental principle of program organization.
Dijkstra's Original Insight:
"Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. We know that a program must be correct and we can study it from that viewpoint only; we also know that it should be efficient and we can study its efficiency on another day, so to speak. In another mood we may ask ourselves whether, and if so: why, the program is desirable. But nothing is gained—on the contrary!—by tackling these various aspects simultaneously. It is what I sometimes have called 'the separation of concerns', which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts."
This insight remains as powerful today as when it was written. A concern represents a coherent area of interest that can be reasoned about independently—even though in the final system, all concerns must work together harmoniously.
Identifying concerns is an analytical skill that develops with experience. A junior developer might see a monolithic 'handle user request' function. A senior developer sees authentication, authorization, validation, business logic, persistence, and response formatting—each a distinct concern waiting to be recognized.
Concerns manifest at different levels and serve different purposes within software systems. Understanding these categories helps you systematically analyze any system and ensure you haven't overlooked important aspects.
Functional vs Non-Functional Concerns:
The most fundamental distinction is between concerns that define what the system does versus how well it does it:
| Category | Definition | Examples | Typical Questions |
|---|---|---|---|
| Functional Concerns | What the system does—its features and capabilities | User registration, payment processing, order fulfillment, report generation | What actions can users perform? What data transformations occur? |
| Non-Functional Concerns | How well the system performs its functions—quality attributes | Performance, security, reliability, scalability, usability, maintainability | How fast? How secure? How available? How easy to modify? |
Domain Concerns vs Technical Concerns:
Another critical distinction separates concerns related to the problem domain from concerns related to the technical solution:
Domain concerns and technical concerns typically change for different reasons and at different rates. Business rules evolve with market conditions; infrastructure concerns change with technology choices. Separating them means that replacing your database doesn't require rewriting your business logic, and changing a business rule doesn't require understanding your caching strategy.
While every application has unique functional requirements, certain concerns appear so consistently across software systems that they form a common vocabulary for engineers. Recognizing these patterns accelerates your ability to analyze new systems and communicate with other developers.
The Universal Concerns:
Consider a typical web application processing a user request. Even in a simple CRUD operation, numerous concerns are involved:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// A seemingly simple function that actually addresses many concernsasync function createOrder(request: CreateOrderRequest): Promise<OrderResponse> { // CONCERN: Input Validation // Ensuring data meets structural and semantic requirements validateOrderRequest(request); // CONCERN: Authentication // Verifying the identity of the requester const user = await authenticateRequest(request.token); // CONCERN: Authorization // Determining if the authenticated user can perform this action await authorizeOrderCreation(user, request); // CONCERN: Business Logic / Domain Rules // Applying domain-specific calculations and validations const orderTotal = calculateOrderTotal(request.items); const discounts = applyDiscountRules(orderTotal, user); const finalTotal = orderTotal - discounts; // CONCERN: Inventory Management // Checking and reserving stock await reserveInventory(request.items); // CONCERN: Persistence // Storing data durably const order = await orderRepository.save({ userId: user.id, items: request.items, total: finalTotal, status: 'pending' }); // CONCERN: External Integration // Communicating with payment provider await paymentService.authorize(order.id, finalTotal); // CONCERN: Event Publishing / Notification // Informing other parts of the system await eventBus.publish('order.created', { orderId: order.id }); // CONCERN: Logging / Observability // Recording what happened for debugging and analytics logger.info('Order created', { orderId: order.id, userId: user.id }); // CONCERN: Response Formatting / Presentation // Preparing the output for the caller return formatOrderResponse(order);}The code above is instructive but problematic from a separation of concerns perspective. All these concerns are intertwined in a single function, making it difficult to:
Recognizing these concerns is the first step. Later in this module, we'll learn techniques to isolate them.
Identifying concerns is part analysis, part intuition—and it improves dramatically with practice. The following techniques provide systematic approaches to uncovering concerns in any system.
Technique 1: The 'Why' Chain
For any piece of code or functionality, ask 'Why does this exist?' repeatedly until you reach a fundamental purpose. Each distinct answer often reveals a separate concern.
Example: Why does this line validate the email format? → To ensure data quality (Input Validation concern) → Why ensure data quality? To prevent errors later (Error Prevention concern) → Why prevent errors? To maintain system integrity (Data Integrity concern)
Each level reveals a different aspect that might warrant separate consideration.
12345678910111213141516171819202122
// Original mixed concern codefunction processPayment(payment: Payment) { // Why? To ensure the amount is valid (VALIDATION) if (payment.amount <= 0) throw new Error("Invalid amount"); // Why? To track what happened (LOGGING) console.log(`Processing payment: ${payment.id}`); // Why? To check user permission (AUTHORIZATION) if (!payment.user.canMakePayments) throw new Error("Not authorized"); // Why? To persist the payment record (PERSISTENCE) database.save(payment); // Why? To communicate with payment provider (INTEGRATION) stripeClient.charge(payment.amount, payment.user.card); // Why? To inform other systems (EVENTING) eventBus.emit('payment.processed', payment);} // Each "Why?" answer points to a distinct concern that could be separatedTechnique 2: The Stakeholder Lens
Different stakeholders care about different aspects of a system. Viewing the system through each stakeholder's perspective reveals concerns that might otherwise be invisible:
Each stakeholder perspective illuminates different concerns.
Ask: 'What different forces might cause this code to change?' Each distinct force typically represents a distinct concern. If marketing requirements, database schema changes, and regulatory updates could all modify the same code, you have multiple concerns entangled. Well-separated concerns change for a single reason each.
Technique 3: The Verbal Description Test
Describe what a component does in plain English. If you find yourself using the word 'and' frequently, you're likely describing multiple concerns:
Poor (multiple concerns): "This class handles user registration and sends welcome emails and logs the new user and validates email uniqueness."
Better (focused concerns):
Each simple description points to a single concern.
Concerns exist at multiple levels of granularity. A common mistake is applying separation of concerns at too fine or too coarse a level. Understanding the hierarchy of concerns helps you make appropriate decisions about where to invest in separation.
Concern Hierarchy:
Concerns can be analyzed at progressively finer levels of detail:
| Level | Scope | Examples | Typical Separation Mechanism |
|---|---|---|---|
| System | Entire product or platform | E-commerce vs CMS vs Analytics | Separate systems, APIs, deployments |
| Subsystem/Bounded Context | Major functional area | Ordering, Inventory, Payments, Shipping | Microservices, modules, packages |
| Component/Service | Coherent functional unit | Order validation, price calculation, notification | Classes, services, functions |
| Aspect | Cross-cutting behavior | Logging, security, transaction management | Aspects, middleware, decorators |
| Method/Function | Single operation | Calculate discount, format date, validate email | Functions, private methods |
12345678910111213141516171819202122232425262728293031323334353637383940414243
// SYSTEM LEVEL: Entire products/platforms// - OrderingSystem// - InventorySystem// - PaymentGateway // SUBSYSTEM LEVEL: Bounded contexts within a productnamespace OrderingContext { // Everything related to orders class OrderService { /* ... */ } class OrderRepository { /* ... */ } class OrderEvents { /* ... */ }} namespace PaymentContext { // Everything related to payments class PaymentService { /* ... */ } class PaymentRepository { /* ... */ } class PaymentEvents { /* ... */ }} // COMPONENT LEVEL: Services within a bounded contextclass OrderService { constructor( private validator: OrderValidator, // Validation concern private calculator: PriceCalculator, // Calculation concern private repository: OrderRepository, // Persistence concern private events: OrderEventPublisher // Event concern ) {}} // ASPECT LEVEL: Cross-cutting concerns// Applied via decorators/middleware@Transactional()@Logged()@Cached({ ttl: 300 })async getOrderById(id: string): Promise<Order> { ... } // METHOD LEVEL: Single operationsclass PriceCalculator { calculateBasePrice(items: Item[]): Money { /* focused logic */ } applyDiscounts(price: Money, discounts: Discount[]): Money { /* focused logic */ } applyTax(price: Money, region: Region): Money { /* focused logic */ }}Too fine-grained separation creates fragmentation—dozens of tiny classes that individually make no sense. Too coarse-grained separation fails to achieve any real separation—a few massive classes with everything tangled together. The right granularity is where each unit has a clear, coherent purpose and changes for a single reason. This is art as much as science.
The concept of concerns transcends any specific programming paradigm, but different paradigms offer different mechanisms for expressing and separating concerns.
Object-Oriented Programming:
OOP provides classes and interfaces as the primary mechanism for separating concerns. Each class ideally represents a single concern, and interfaces define the contracts between concerns:
1234567891011121314151617181920212223242526272829
// OOP: Classes encapsulate concernsinterface OrderRepository { save(order: Order): Promise<void>; // Persistence concern findById(id: string): Promise<Order>;} interface PaymentProcessor { process(amount: Money): Promise<PaymentResult>; // Payment concern} interface NotificationService { notify(user: User, message: Message): Promise<void>; // Notification concern} // Concerns are composed via dependency injectionclass OrderService { constructor( private repository: OrderRepository, private payment: PaymentProcessor, private notifications: NotificationService ) {} async createOrder(order: Order): Promise<void> { // This method orchestrates but doesn't implement the concerns await this.repository.save(order); await this.payment.process(order.total); await this.notifications.notify(order.user, orderConfirmation(order)); }}Functional Programming:
FP separates concerns through pure functions and composition. Each function addresses a single transformation, and concerns are separated by avoiding side effects:
12345678910111213141516171819202122232425262728293031323334
// FP: Functions represent concerns as pure transformations// Each function handles one concern // Validation concern (pure)const validateOrder = (order: Order): Either<ValidationError[], Order> => { const errors = [ validateItems(order.items), validateCustomer(order.customer), validateShipping(order.shipping) ].filter(isError); return errors.length > 0 ? left(errors) : right(order);}; // Calculation concern (pure) const calculateTotal = (order: Order): OrderWithTotal => ({ ...order, total: order.items.reduce((sum, item) => sum + item.price * item.quantity, 0)}); // Discount concern (pure)const applyDiscounts = (order: OrderWithTotal): OrderWithTotal => ({ ...order, total: order.total * (1 - getDiscountRate(order))}); // Composition: pipeline of concernsconst processOrder = pipe( validateOrder, map(calculateTotal), map(applyDiscounts), chain(persistOrder), // Side effect isolated chain(sendNotification) // Side effect isolated);Aspect-Oriented Programming:
AOP provides explicit mechanisms for separating cross-cutting concerns (those that span multiple components) from primary logic:
12345678910111213141516171819202122232425262728293031323334353637383940
// AOP: Cross-cutting concerns declared separately from business logic // Business logic - clean and focusedclass OrderService { async createOrder(order: Order): Promise<Order> { const validated = this.validator.validate(order); const priced = this.calculator.calculateTotal(validated); return this.repository.save(priced); }} // Cross-cutting concerns declared as aspects@Aspect()class LoggingAspect { @Before('execution(* OrderService.*(..))') logEntry(jp: JoinPoint) { console.log(`Entering ${jp.methodName} with args: ${jp.args}`); } @After('execution(* OrderService.*(..))') logExit(jp: JoinPoint) { console.log(`Exiting ${jp.methodName}`); }} @Aspect()class TransactionAspect { @Around('execution(* *Repository.save(..))') wrapInTransaction(jp: JoinPoint) { const tx = this.transactionManager.begin(); try { const result = jp.proceed(); tx.commit(); return result; } catch (e) { tx.rollback(); throw e; } }}Regardless of paradigm, the goal remains the same: organize code so that each piece has a clear, single purpose and can be understood, modified, and tested in relative isolation. The paradigm provides the tools; the principle guides their use.
Let's apply what we've learned to analyze real code and identify its concerns. These exercises build the analytical muscle needed for effective separation.
Exercise 1: The God Function
Examine the following function and identify all distinct concerns:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
async function handleUserRegistration(req: Request, res: Response) { // What concerns can you identify here? const startTime = Date.now(); const { email, password, name } = req.body; if (!email || !email.includes('@')) { return res.status(400).json({ error: 'Invalid email' }); } if (!password || password.length < 8) { return res.status(400).json({ error: 'Password too short' }); } try { const existing = await db.users.findOne({ email }); if (existing) { return res.status(409).json({ error: 'Email already registered' }); } const hashedPassword = await bcrypt.hash(password, 10); const user = await db.users.insert({ email, password: hashedPassword, name, createdAt: new Date() }); const token = jwt.sign({ userId: user.id }, SECRET, { expiresIn: '24h' }); await sendEmail({ to: email, subject: 'Welcome!', body: `Welcome to our platform, ${name}!` }); await analytics.track('user.registered', { userId: user.id }); console.log(`User ${email} registered in ${Date.now() - startTime}ms`); res.cookie('authToken', token, { httpOnly: true }); res.status(201).json({ user: { id: user.id, email, name } }); } catch (error) { console.error('Registration failed:', error); await alerting.notify('registration-failure', error); res.status(500).json({ error: 'Registration failed' }); }}Notice how this 'simple' function embeds 12+ distinct concerns in roughly 50 lines. Each concern could change independently: validation rules might evolve, email templates might change, analytics providers might switch. Yet they're all tangled together. In the next pages, we'll learn techniques for isolating these concerns, making each one testable and modifiable on its own.
Identifying concerns is the foundational skill for separation of concerns. Before you can separate, you must see. Let's consolidate what we've learned:
What's Next:
Now that we can identify concerns, the next page explores Isolating Concerns—the techniques and patterns for actually achieving separation in code. We'll learn how to structure systems so that each concern lives in its own space, interacting with others through well-defined boundaries.
You now understand what concerns are and how to identify them systematically. This analytical skill is the foundation for all separation of concerns work—you can't separate what you can't see. In the next page, we'll learn how to translate this understanding into structured, well-organized code.