Architecture - Logical segregation of Concerns


The following section examines significant paradigms over the segregation of concerns realm in Applicative Service-Level Architecture.

Applicative Level Architecture

We propose the best way to discuss and contextualize the following architecture paradigms would be to focus on:
-- Introduction to the paradigm: A brief overview of the architectural style, its purpose, and its key principles.
-- The Protagonists: The core building blocks (e.g., modules, layers, or services) that define the structure of the architecture.
-- The Dependency Flow: How dependencies are managed and the direction of interactions between components, emphasizing separation of concerns and modularity. We will take a back end API approach to applicative design.

Layered (N-Tier) Architecture

  • Introduction: Layered architecture organizes code into distinct layers (e.g., presentation, business logic, data access), each with specific responsibilities. It promotes modularity

  • Protagonists:

    • Presentation Layer: Handles user interface or API endpoints (e.g., REST controllers in Spring or Express.js).
    • Business Logic Layer: Contains core application logic, services, or domain models.
    • Data Access Layer: Manages database interactions, often via repositories or ORMs (e.g., Hibernate in Java, Sequelize in JavaScript).
    • Infrastructure Layer (optional): Handles cross-cutting concerns like logging or configuration.
  • Dependency Flow:

    • Dependencies flow downward: the presentation layer depends on the business logic layer, which depends on the data access layer.
    • Each layer interacts only with the layer immediately below it, enforcing a strict hierarchy.
    • The data access layer depends on the database or external systems.

Domain-driven design (DDD)

by Eric Evans

  • Introduction: Domain-Driven Design modeling the software to match the domain, or the subject area, that the software is intended for, using ubiquitous language, bounded contexts, and domain entities

  • Protagonists:

    • Entities and Value Objects: Core domain objects representing business concepts with identity (entities) or immutable attributes (value objects).
    • Aggregates: Clusters of entities and value objects with a single root, enforcing consistency boundaries.
    • Domain Services: Stateless services encapsulating complex business logic that doesn’t fit in entities.
    • Repositories: Interfaces for accessing aggregates, typically implemented in the infrastructure layer.
    • Bounded Contexts: Logical boundaries defining a specific domain model and its scope.
  • Dependency flow:

    • The domain layer (entities, aggregates, domain services) is independent, focusing solely on business logic.
    • Application services depend on the domain layer, orchestrating use cases and invoking domain services or aggregates.
    • Repositories (defined in the domain layer) are implemented in the infrastructure layer, which depends on the domain layer’s interfaces.
    • Dependencies flow inward, with the infrastructure layer (databases, external APIs) depending on the domain and application layers, ensuring domain isolation.

Onion Architecture

by Jeffrey Palermo

  • Introduction: Onion architecture, by Jeffrey Palermo, organizes code in concentric layers, with the domain model at the core and dependencies flowing inward.

  • Protagonists:

    • Domain Model: The core, containing entities, value objects, and business rules.
    • Domain Services: Business logic that operates on domain models, still part of the core.
    • Application Services: Orchestrate use cases, coordinating domain logic and external interactions.
    • Infrastructure Layer: Handles external concerns like databases, APIs, or UI, implementing interfaces defined by inner layers.
  • Dependency flow:

    • Dependencies flow inward: the infrastructure layer depends on application services, which depend on domain services and the domain model.
    • The domain model is independent, with no knowledge of outer layers.
    • Application services define interfaces for infrastructure (e.g., repository interfaces), which the infrastructure layer implements.

Hexagonal (a.k.a Ports and Adapters) Architecture

introduced by Alistair Cockburn

  • Introduction: Hexagonal architecture isolates business logic from external systems through ports (interfaces) and adapters (implementations), enhancing modularity, testability, and maintainability.

  • Protagonists:

    • Core (Business Logic): Contains domain models and business rules, independent of external systems.
    • Ports: Interfaces defining interactions between the core and external systems (e.g., database access, API calls).
    • Adapters: Concrete implementations of ports, connecting the core to external systems (e.g., REST controllers, database drivers).
    • External Systems: Databases, APIs, or message brokers interacted with via adapters.
  • Dependency flow:

    • The core depends only on ports, not on adapters or external systems.
    • Adapters depend on ports and external systems, implementing the interfaces to connect the core to the outside world.
    • Dependencies flow inward: external systems → adapters → ports → core, ensuring the core remains isolated and testable.
    • This inversion of dependencies (via ports) decouples the core from implementation details.

Clean Architecture

by Robert Martin

  • Introduction: Clean Architecture, a collective term for architectural variants (including Hexagonal, Onion, DDD), emphasizes separation of concerns, with business logic at the core and dependencies flowing inward. It prioritizes testability, maintainability, and independence from tooling (frameworks, libraries, databases, etc.).

  • Protagonists:

    • Entities: Core business objects, encapsulating business rules, independent of external systems.
    • Use Cases: Application-specific business rules, orchestrating interactions with entities.
    • Interface Adapters: Convert data between use cases and external systems (e.g., controllers, presenters).
    • Frameworks and Drivers: External systems (databases, UI, APIs) interacting via adapters.
  • Dependency Flow:

    • Dependencies flow inward (like Onion): frameworks and drivers depend on interface adapters (somewhat like hexagonal), which depend on use cases, which depend on entities.
    • Entities are independent, containing pure business logic.
    • Use cases define interfaces for adapters (e.g., repository or controller interfaces), which external systems implement.

Contextualizing Separation of Concerns

At this point it may feel confusing, as in what does what and how they interweave. Let's recap: Hexagonal, Clean, Onion, and Layered architectures all follow separation of concerns, organizing applications into distinct layers with specific responsibilities. The critical differences between these strands is their boundary definitions and dependency interactions: Comprehensively:

  • Layered Architecture uses a hierarchical dependency flow (top-to-bottom), often risking tight coupling.
  • Domain Driven Development architecture provides a domain-centric applicative design approach.
  • Onion Architecture emphasizes inward dependency flow and dependency inversion, with at it's core the domain layer -> application layer -> infrastructure layer at the periphery.
  • Hexagonal Architecture, a strand of Onion, too inverts dependencies using ports and adapters, isolating the core from external systems.
  • Clean Architecture generalizes the above principles, prioritizing independence from frameworks and testability.

Conclusion

It may seem challenging to select the appropriate approaches, with intent to ensuring practical integration, and fostering a maintainable, simple codebases across diverse team cultures and maturity levels. We promote adopting a standardized, enterprise-wide development practice. This "one-size-fits-all" approach enhances applicative maintenance pathways and codebase simplicity, extending beyond the initial component-service proof-of-concept phases.

Predictable codebases reduce time-consuming debates about methodologies (e.g., "how we do things here" or "things to do/not to do"). An enterprise culture and practices should eventually converge and aim for uniformity as to simplifies decision-making and aligns teams on common practices.

This in turn tends to:

  • Produces a company cohesive strategy to development practices. (++ employability).
  • Minimize technical complexity to focus on domain evolution.
  • Straightforwards the applicative build processes.
  • Increases infrastructure and technical consolidations.
  • Reduced need for extensive refactoring (where evolution primarily require rewiring).
  • Enhanced exploration of new techniques with minimal disruption.
  • Enable broader and robust test coverage - where time saved to be allocated to wider testing practices / (Unit Testing, End-to-End (E2E) Testing, Chaos Testing, Monkey/Gorilla Testing, Performance Testing, Contract Testing, Mutation Testing, Security Testing, Acceptance Testing, Smoke Testing).
  • Produces precise workload estimates and eventually lower development costs (predictability now batched by AI).

To implement this unified practice effectively:

  • Define: Create a clear, documented framework for development practices, including coding standards, architectural patterns, and testing guidelines.
  • Align: Engage teams through workshops and training to ensure buy-in and consistent adoption.
  • Enforce: Use automated tools (e.g., code reviews, CI/CD checks) to maintain adherence to standards.
  • Iterate: Periodically assess and update the framework to address new challenges and incorporate feedback from teams.

As an example, see here for preconized web service architecture.

Done!

Thanks for reading! We hope this overview of applicative architecture paradigms helps you understand the principles of separation of concerns and the various related attempts over ceremonial paradigms. For more detailed explanations, follow the links provided to each specific pattern.