From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%

We had activity logging across our entire application – typical audit trail stuff that tracks user actions throughout the system. The initial implementation was straightforward: add logging calls directly in the service methods.

It worked perfectly. S…


This content originally appeared on DEV Community and was authored by Digvijay Jadhav

We had activity logging across our entire application - typical audit trail stuff that tracks user actions throughout the system. The initial implementation was straightforward: add logging calls directly in the service methods.

It worked perfectly. Shipped on time, no issues in production. But after a few months of adding new features, the pattern became obvious - we had the same logging boilerplate repeated across dozens of methods.

Not broken. Not urgent. Just... inefficient.

Here's what the pattern looked like:

async someServiceMethod(
  userId: string,
  data: string,
  context?: { ipAddress?: string; userAgent?: string }
) {
  try {
    const result = await performOperation(userId, data);
    *// Success logging - 12 lines every single time*
    this.activityLogService
      .logActivity({
        userId,
        actionType: "RESOURCE_ACTION",
        resourceId: result.id,
        resourceName: result.name,
        ipAddress: context?.ipAddress,
        userAgent: context?.userAgent,
        status: "SUCCESS",
      })
      .catch((err) => {
        console.error("Failed to log activity:", err);
      });
    return result;
  } catch (error) {
    *// Failure logging - another 12 lines*
    this.activityLogService
      .logActivity({
        userId,
        actionType: "RESOURCE_ACTION",
        resourceName: data,
        ipAddress: context?.ipAddress,
        userAgent: context?.userAgent,
        status: "FAILED",
        errorMessage: error instanceof Error ? error.message : "Unknown error",
      })
      .catch((err) => {
        console.error("Failed to log activity failure:", err);
      });
    throw error;
  }
}

Multiply this by every service method that needed logging - we're talking about hundreds of lines of repetitive try-catch blocks doing essentially the same thing.

The One-Day Refactor Decision

We then thought of optimizing this, This is a perfect use case for decorators.

The decision was straightforward - we had a cross-cutting concern that was cluttering business logic. Decorators would let us declare the logging behavior and keep the methods focused on what they actually do.

Not a revolutionary insight. Just recognizing when the right tool fits the problem.

The Target Design

The goal was simple - declarative logging that doesn't clutter the business logic:

@LogActivity({
  actionType: "RESOURCE_ACTION",
  resourceType: "RESOURCE"
})
async someServiceMethod(
  userId: string,
  data: string,
  context?: { ipAddress?: string; userAgent?: string }
) {
  const result = await performOperation(userId, data);
  return result;
}

Clean business logic, logging concern declared at the method level. That's it.

Implementation

Configuration

TypeScript decorators need to be enabled:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Interface Design

The decorator configuration needed to handle different method signatures and extract resource information from both successful results and failed attempts:

export interface LogActivityConfig {
  actionType: string;
  resourceType: string;

  paramMapping?: {
    userId?: number | string;  *// Supports both index and nested paths*
    context?: number;
  };

  extractResource?: (result: any, params: any[]) => {
    resourceId?: string;
    resourceName?: string;
    metadata?: any;
  };

  extractResourceFromParams?: (params: any[]) => {
    resourceId?: string;
    resourceName?: string;
    metadata?: any;
  };
}

Key design decisions:

  • Flexible parameter mapping for different method signatures
  • Separate extraction functions for success/failure cases
  • Optional metadata support for additional context

Decorator Implementation

export function LogActivity(config: LogActivityConfig) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    const activityLogService = ActivityLogService.getInstance();

    descriptor.value = async function (...args: any[]) {
      *// Extract userId from parameters*
      let userId: string;
      if (typeof config.paramMapping?.userId === "string") {
        const parts = config.paramMapping.userId.split(".");
        let value: any = args[0];
        for (const part of parts) {
          value = value?.[part];
        }
        userId = value;
      } else {
        const userIdIndex = config.paramMapping?.userId ?? 0;
        userId = args[userIdIndex];
      }

      const contextIndex = config.paramMapping?.context ?? args.length - 1;
      const context = args[contextIndex];

      try {
        const result = await originalMethod.apply(this, args);

        const { resourceId, resourceName, metadata } =
          config.extractResource?.(result, args) || {};

        *// Non-blocking success logging*
        activityLogService
          .createActivityLog({
            userId,
            actionType: config.actionType,
            resourceType: config.resourceType,
            resourceId,
            resourceName,
            metadata,
            ipAddress: context?.ipAddress,
            userAgent: context?.userAgent,
            status: "SUCCESS",
          })
          .catch((err) => 
            console.error(`Failed to log ${config.actionType}:`, err)
          );

        return result;

      } catch (error) {
        const { resourceId, resourceName, metadata } =
          config.extractResourceFromParams?.(args) || {};

        *// Non-blocking failure logging*
        activityLogService
          .createActivityLog({
            userId,
            actionType: config.actionType,
            resourceType: config.resourceType,
            resourceId,
            resourceName,
            metadata,
            ipAddress: context?.ipAddress,
            userAgent: context?.userAgent,
            status: "FAILED",
            errorMessage: error instanceof Error ? error.message : "Unknown error",
          })
          .catch((err) => 
            console.error(`Failed to log ${config.actionType}:`, err)
          );

        throw error;
      }
    };

    return descriptor;
  };
}

Critical implementation details:

  • Logging operations are non-blocking (won't break main functionality)
  • Original errors are always re-thrown unchanged
  • Decorator preserves original method behavior completely

Results

Before (213 lines across a service):

export class ServiceClass {
  private activityLogService: ActivityLogService;

  constructor() {
    this.activityLogService = ActivityLogService.getInstance();
  }

  async someMethod(userId: string, data: string, context?: Context) {
    try {
      const result = await performOperation(userId, data);

      this.activityLogService
        .logActivity({
          userId,
          actionType: "ACTION_TYPE",
          resourceId: result.id,
          resourceName: result.name,
          ipAddress: context?.ipAddress,
          userAgent: context?.userAgent,
          status: "SUCCESS",
        })
        .catch((err) => console.error("Failed to log:", err));

      return result;
    } catch (error) {
      this.activityLogService
        .logActivity({
          userId,
          actionType: "ACTION_TYPE",
          resourceName: data,
          ipAddress: context?.ipAddress,
          userAgent: context?.userAgent,
          status: "FAILED",
          errorMessage: error.message,
        })
        .catch((err) => console.error("Failed to log:", err));

      throw error;
    }
  }

  *// Same pattern repeated for every method...*
}

After (139 lines - 35% reduction):

export class ServiceClass {
  @LogActivity({
    actionType: "ACTION_TYPE",
    resourceType: "RESOURCE",
    paramMapping: { userId: 0, context: 2 },
    extractResource: (result) => ({
      resourceId: result.id,
      resourceName: result.name,
    }),
    extractResourceFromParams: (params) => ({
      resourceName: params[1],
    }),
  })
  async someMethod(userId: string, data: string, context?: Context) {
    const result = await performOperation(userId, data);
    return result;
  }

  @LogActivity({
    actionType: "ANOTHER_ACTION",
    resourceType: "RESOURCE",
    paramMapping: { userId: 0, context: 3 },
    extractResource: (result, params) => ({
      resourceId: result.id,
      resourceName: result.name,
      metadata: { additionalInfo: params[2] },
    }),
  })
  async anotherMethod(
    userId: string,
    data: string,
    info: string,
    context?: Context
  ) {
    const result = await performAnotherOperation(userId, data, info);
    return result;
  }
}

The reduction isn't just about line count - the code is now focused on business logic with cross-cutting concerns handled declaratively.

Impact

Quantitative:

  • 70% reduction in logging-related code
  • 35% overall reduction per service
  • Zero changes to business logic behavior
  • Maintained 100% test coverage

Qualitative:

Improved Onboarding
New developers can immediately understand method intent without parsing logging infrastructure:

typescript

@LogActivity({ actionType: "RESOURCE_ACTION", resourceType: "RESOURCE" })
async someMethod(userId: string, data: string, context?: Context) {
*// Pure business logic*
}

Faster Feature Development
Adding logging to new methods: add decorator, configure parameters, done. No boilerplate to copy, no edge cases to remember.

Simplified Maintenance
Need to change logging format globally? Update the decorator. One change propagates everywhere.

Better Code Reviews
Reviewers focus on business logic. Cross-cutting concerns are declared, not mixed in with implementation.

When This Pattern Applies

Decorators solve a specific problem: you have behavior that needs to be applied consistently across multiple methods, but that behavior is orthogonal to the core business logic.

Good indicators:

  • Same try-catch pattern across multiple methods
  • Cross-cutting concerns mixed with business logic
  • Copy-pasting setup/teardown code
  • Consistent "before" and "after" logic

Other Applications

Once the logging decorator was in place, we identified similar opportunities:

Authentication & Authorization:

@RequireAuth()
@RequirePermission("resource.delete")
async deleteResource(resourceId: string, userId: string) {
  *// Just deletion logic*
}

Rate Limiting:

@RateLimit({ maxRequests: 10, windowMs: 60000 })
async processRequest(data: RequestData, userId: string) {
  *// Just request processing*
}

Performance Monitoring:

@MeasurePerformance({ threshold: 1000, alertOn: "slow" })
async complexOperation(params: OperationParams) {
  *// Just operation logic*
}

Caching:

@Cache({ ttl: 300, key: (userId) => `user:${userId}:data` })
async getUserData(userId: string) {
  *// Just data retrieval*
}

Implementation Approach

The refactor was straightforward:

  1. Built the decorator with proper TypeScript typing
  2. Applied to one service and verified behavior
  3. Rolled out incrementally across services
  4. Added documentation and examples

Total time: one day of focused work.

Key Takeaways

Pragmatism First, Optimization Second
The original implementation wasn't wrong - it worked in production without issues. The decorator refactor was an optimization made when the pattern became clear, not a premature abstraction.

Design Patterns as Refactoring Tools
Patterns are most valuable when you recognize them in existing code, not when you try to force them during initial implementation.

Incremental Adoption
Starting with one service and expanding proved less risky than a wholesale rewrite. Validate the pattern works before committing to it everywhere.

Clear Over Clever
The decorator doesn't make the code sophisticated - it makes it clear. Methods now explicitly declare their concerns rather than embedding them in implementation.

Conclusion

This refactor wasn't about applying design patterns for their own sake. It was about recognizing a specific problem - repetitive cross-cutting concerns cluttering business logic - and using the appropriate tool to solve it.

The result: less code, better maintainability, and clearer separation of concerns. Sometimes the best refactor is the one you didn't do upfront, but recognized when the need became obvious.


This content originally appeared on DEV Community and was authored by Digvijay Jadhav


Print Share Comment Cite Upload Translate Updates
APA

Digvijay Jadhav | Sciencx (2025-11-07T10:50:06+00:00) From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%. Retrieved from https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/

MLA
" » From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%." Digvijay Jadhav | Sciencx - Friday November 7, 2025, https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/
HARVARD
Digvijay Jadhav | Sciencx Friday November 7, 2025 » From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%., viewed ,<https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/>
VANCOUVER
Digvijay Jadhav | Sciencx - » From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/
CHICAGO
" » From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%." Digvijay Jadhav | Sciencx - Accessed . https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/
IEEE
" » From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70%." Digvijay Jadhav | Sciencx [Online]. Available: https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/. [Accessed: ]
rf:citation
» From Repetitive Code to Clean Architecture: How the Decorator Pattern Simplified Activity Logging by 70% | Digvijay Jadhav | Sciencx | https://www.scien.cx/2025/11/07/from-repetitive-code-to-clean-architecture-how-the-decorator-pattern-simplified-activity-logging-by-70/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.