Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
Skip to main content

Avoiding fat service classes

Some ways to avoid service classes getting bigger and bigger

Basic idea

  • In a lot of applications, you have service classes that act as a facade for lower-level domain logic and also contain coordination and control logic
  • These service classes tend to grow over time and can become so "fat" that they are difficult to maintain

Approach: split into sub-services

Basic idea:

  • Identify different areas of functionality within the fat service class
    • Potential way to split: identify sub-concepts
    • Potential way to split: retrieval of data vs. changing data
  • Create a separate service for each area
  • If needed, you can create a helper service for sharing common functionality
  • Either the old fat service class acts as a facade to these more specific service classes, or clients call the sub-services directly

Approach: delegate to focused classes

Basic idea:

  • Identify the actions that the fat service class is performing
    • This could be as simple as "1 public method = 1 action"
  • Create a separate class to represent each action
    • It might make sense to have all of these actions implement a common interface, especially if there are some "cross-cutting concerns" that need to be taken care of regardless of the specific type of action
  • The old fat service class creates the actions and then delegates to them
    • Typically each call to the fat service class will create one or more instances of the action classes based on the specific input received
  • The old fat service class takes care of cross-cutting concerns if needed

This can be seen as a form of the Command pattern

Benefits:

  • Class per action means that we get some very focused classes
  • Class per action means we can easily compose higher-level actions out of lower-level actions
  • This approach makes it relatively easy to provide undo functionality or show a history of actions (if needed)

Example: virtual file system

  • Situation:
    • Fat service class with functionality for file creation, file update, file deletion, replacing a folder's contents with contents from an archive, ...
    • For every call to the service class, all changes must happen within a single DB transaction
    • All changes generate events that other components can listen to, plus they generate updates to an in-memory representation of the current state of the file system. However, those events and updates are only valid once the entire transaction is committed.
  • Solution:
    • Each file action (create, update, delete, ...) is implemented as its own class
    • All of these file action classes share the same interface, which specifies that they return events and in-memory cache updates
    • Bigger actions (archive import etc.) delegate work to smaller actions that they create
    • For every call to the service class, it creates the necessary actions based on the received input and also passes a transactional DB connection on construction
    • Service class collects event and cache updates
    • When all actions for a service class call have completed, the service class commits the DB transaction, sends events and applies updates to the in-memory file system representation
    • In this particular case, file actions depended on several lower-level services. Solution: file action factory service with methods for creating each type of action. Each method takes action input + DB connection and calls the action's constructor with action input, DB connection and instances of lower level services.
export interface FileAction {
public executeAndGetResult(): Promise<FileActionResult>;
}

export interface FileActionResult {
events: FileEvent[];
cacheUpdates: FileCacheUpdate[];
}

Resources