The Service Layer Pattern
One of the key ingredients in managing Enterprise software products is identifying and consistently applying a proven, battle-tested architecture. Without an over-arching architectural outline the end result of a constantly evolving product will likely be a sprawling, deeply-coupled codebase that's brittle and hard to work with.
Of course, having an architectural plan is not enough. All your team members (or at least the senior devs) need to "buy in" and be settled on the rationale supporting the plan.
When evaluating architectural frameworks I look for ones that facilitate...
1. System Comprehension
2. Modularity (Separation of Concerns)
3. Testability (Explicit Dependencies)
4. Hexagonality (Transport Agnosticism)
5. Cross-cutting concerns
One of the Architectural patterns that I've found very useful in this regard is the Service Layer Pattern. I find that combining this with a couple of other design patterns gives me a lightweight yet robust architectural framework for large-scale applications.
Here are the steps I recommend:
Define Modules: Languages like python and Node.js offer built-in ways of defining modules. But modules are less of a software artifact than a domain concept. A module could just be a folder. The key idea is to define module boundaries based on function. A module then becomes a cohesive silo of code focused on solving one set of closely-related tasks. Modules will typically have some Data or Persistence Models and should be self-contained.
Wrap each Module task into it's own Command. The easiest way to do this is to define a common Command Interface that makes instantiating and using Commands uniform across the codebase.
Write Services that are composed of Module Commands: The aim of a Service is to act as the public API of the core platform. The methods in each Service should correspond to distinct use cases for the system as a whole. A Service should wrap itself around one module and use it's commands to satisfy each use case. The Service then becomes a great place to handle cross-cutting concerns (Authorization, Auditing etc).
Use Events to as hooks to prevent coupling: Services serve as the publishers of Domain and System Events. An Event-driven architecture is a very elegant way of preventing coupling, while allowing significant events to propagate across the software system in order to accomplish auxiliary tasks. This is key to building a plugin-oriented architecture, where parts of the system maybe be turned on and off at will.
Inject Service dependencies using a Dependency Injection Container. Commands often need access to the functionality of other Services and the parent Service will need to inject this while instantiating the Command. This is where a DI Container is useful -- it allows you to separate the configuring and instantiating of external Services (handled by the DI Container) from their usage at the Service layer. The Container should itself never be used below the Service layer. This makes dependencies explicit both for comprehension and testing, since the injected Service can be trivially mocked.
The relevant Design Patterns are:
- the Service Layer Pattern
- the Container Pattern
- the Subscriber Pattern
- the Command Pattern
The Service layer then is all that your transport layer (HTTP Controller, CLI client etc) will know about and interact with, which results in a hexagonal or symmetric system.
One major (& often overlooked) advantage of a Service Layer is the ability to progressively split a monolithic codebase into a suite of micro-services (for scalability) with the least amount of effort.
There you have it: Modules, Commands, Events, Services and a Container together providing a layered and modular enterprise architecture framework.