Redefining the Service Layer with Groovy Categories

Developing applications in an environment with evolving requirements can be difficult. You may notice, as is true with most projects (even the most well tested projects), that domain logic can become unmaintainable as the underlying business requirements mutate over time. Having a good application architecture means that your code will be flexible enough to adapt to new requirements as they come.

Groovy’s dynamic nature offers us a great deal of flexibility with respect to application design. We can leverage these powerful dynamic offerings to garner the benefits of DRY, maintainable, and unit testable code in really understandable and easy-to-follow ways that we wouldn’t get from Plain Old Java. In this article, in a real world scenario-based discussion, I will demonstrate how to employ Groovy Categories to facilitate the implementation of evolving business requirements.

To start with, let’s discuss how you might approach implementing a payment processor with some business requirements around how to interact with the different payment gateways.

In this scenario, your company wants to offer a variety of payment methods, including corporate gift cards and all major credit cards. After researching, they’ve found a single company to handle authorizing major credit cards, and another company to handle issuance, support, and authorization of corporate gift cards. Both companies have a payment gateway interface and a Java API. Given the requirements, it would be reasonable to approach the problem by constructing a data model and creating a service class to handle the processing of the different payment types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Payee {
String name
String phoneNumber
Address billingAddress
}
 
public interface PaymentType {
Payee getPayee();
}
 
class CreditCardPaymentType implements PaymentType {
enum CreditCardType {
VISA, MASTERCARD, AMEX, DISCOVER
}
 
CreditCardType cardType
 
String cardNumber
String securityCode
Date expirationDate
 
Payee payee
}
 
class GiftCardPaymentType implements PaymentType {
String cardNumber
String securityCode
 
Payee payee
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface PaymentService {
/**
* Returns an authorization code
*/
public String process(PaymentType paymentType);
}
 
class LocalPaymentService implements PaymentService {
// Dependency injected from container
CreditCardPaymentGateway creditCardPaymentGateway
GiftCardPaymentGateway giftCardPaymentGateway
 
public String process(PaymentType paymentType) throws IllegalArgumentException {
if (paymentType instanceof CreditCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof GiftCardPaymentType) {
return doProcess(paymentType)
}
throw new IllegalArgumentException("Supplied payment type is not supported.");
}
 
private String doProcess(CreditCardPaymentType paymentType) {
// ...
}
private String doProcess(GiftCardPaymentType paymentType) {
// ...
}
 
}

After a short period of time running with this model and service layer, your company decides that they are going to allow customers to pay by Check. Additionally, they’re also going to allow employees to defer payment for items purchased from your company until their next paycheck. In both cases, your company has found payment vendors to support the new payment type, and those vendors each offer you a payment gateway API. So, you create the appropriate model objects and extend your service layer to incorporate the new payment types. Quickly, you can start to see your paymentType discriminator building itself into a house of cards…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class LocalPaymentService implements PaymentService {
// Dependency injected from container
CreditCardPaymentGateway creditCardPaymentGateway
GiftCardPaymentGateway giftCardPaymentGateway
CheckPaymentGateway checkPaymentGateway
EmployeePaymentGateway employeePaymentGateway
 
public String process(PaymentType paymentType) throws IllegalArgumentException {
if (paymentType instanceof CreditCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof GiftCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof CheckPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof EmployeePaymentType) {
return doProcess(paymentType)
}
throw new IllegalArgumentException("Supplied payment type is not supported.");
}
 
private String doProcess(CreditCardPaymentType paymentType) {
// ...
}
private String doProcess(GiftCardPaymentType paymentType) {
// ...
}
private String doProcess(CheckPaymentType paymentType) {
// ...
}
private String doProcess(EmployeePaymentType paymentType) {
// ...
}
 
}

To demonstrate the fragility of this application architecture, I’ll offer a new business requirement. After considerable analysis, your company has determined that using the Check payment gateway to process California credit card payments will save them money. Now your service layer requires a multi-faceted discriminator to determine which payment gateway is appropriate given the circumstance. Additionally, the workflow for non-credit card payment types may be affected by determinations that are strictly localized to credit card processing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class LocalPaymentService implements PaymentService {
// Dependency injected from container
CreditCardPaymentGateway creditCardPaymentGateway
GiftCardPaymentGateway giftCardPaymentGateway
CheckPaymentGateway checkPaymentGateway
EmployeePaymentGateway employeePaymentGateway
 
public String process(PaymentType paymentType) throws IllegalArgumentException {
if (paymentType instanceof CreditCardPaymentType) {
if ("CA".equalsIgnoreCase(paymentType.getPayee().getBillingAddress().getState())) {
return doCaliforniaCreditProcessing(paymentType)
}
return doProcess(paymentType)
} else if (paymentType instanceof GiftCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof CheckPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof EmployeePaymentType) {
return doProcess(paymentType)
}
throw new IllegalArgumentException("Supplied payment type is not supported.");
}
 
// … [snip] … implementation methods
 
}

Or if you thought you were shrewd and left the payment type discrimination alone and embedded the state discriminator in the corresponding doProcess method, you’ll quickly realize that you’ve only obscured the payment workflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class LocalPaymentService implements PaymentService {
// Dependency injected from container
CreditCardPaymentGateway creditCardPaymentGateway
GiftCardPaymentGateway giftCardPaymentGateway
CheckPaymentGateway checkPaymentGateway
EmployeePaymentGateway employeePaymentGateway
 
public String process(PaymentType paymentType) throws IllegalArgumentException {
if (paymentType instanceof CreditCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof GiftCardPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof CheckPaymentType) {
return doProcess(paymentType)
} else if (paymentType instanceof EmployeePaymentType) {
return doProcess(paymentType)
}
throw new IllegalArgumentException("Supplied payment type is not supported.");
}
 
private String doProcess(CreditCardPaymentType paymentType) {
if ("CA".equalsIgnoreCase(paymentType.getPayee().getBillingAddress().getState())) {
return doProcessCalifornia(paymentType)
}
return doProcessOther(paymentType)
}
 
private String doProcessCalifornia(CreditCardPaymentType paymentType) {
// ...
}
private String doProcessOther(CreditCardPaymentType paymentType) {
// …
}
 
}

Application architecture is all about structuring and building your application in a way that makes compounding requirements easily implemented without affecting code that you already know works. In each of the cases of compounded requirements in this contrived scenario, we were forced to make changes to areas of code that should have remained isolated: when we implemented Check and Employee payment types, we should not have affected the workflow of code that delegates to Credit Card and Gift Card payment types; when we implemented state-based discrimination, we should not have affected the existing workflow for other payment types. Additionally, we’ve made a bad design decision by making all of the payment gateways available. Though it is needed with the above approach, we introduce a risk of error and conceivably a violation of the Single Responsibility Principle; indeed, the credit card processing implementation should not even have the option to make a call to the checkPaymentGateway.

Using Groovy Categories, we can offer a more flexible implementation for the provided scenario. Keeping the data model the same, we’ll approach this problem by reimplementing the service layer with discrete processors that can isolate their payment logic and remote API calls based on the specific payment type. We’ll begin the rewrite by analyzing how we might approach reimplementing the credit card payment processor, since it has the most complex requirements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CreditCardPaymentProcessor {
static {
CreditCardPaymentProcessor.metaClass.static.methodMissing = { String name, args ->
if (name.startsWith("getGatewayFor")) {
def state = name.replaceAll("getGatewayFor","")?.toLowerCase()
switch (state) {
case "ca":
return new CheckPaymentProcessor()
default:
return new CreditCardPaymentProcessor()
}
}
}
}
 
static String process(PaymentType paymentType) {
def gateway = "getGatewayFor${paymentType.payee.billingAddress.state}"()
process gateway, paymentType
}
 
static String process(CheckPaymentGateway paymentGateway, PaymentType paymentType) {
// ...
}
 
static String process(CreditCardPaymentGateway paymentGateway, PaymentType paymentType) {
// …
}
}

In the reimplementation of the credit card processor, perhaps the most noteworthy change is how the processor leverages Groovy’s dynamic nature to retrieve the appropriate payment gateway. Using a little bit of Meta Object Programming, we can hook into Groovy’s missing method handler so that we can create a dynamic finder to resolve the appropriate payment gateway. It’s also important to note that for a method to mixin through use of a Category, the method must be static and the first argument must be an instance of the delegate.

To use the new credit card processor as a Category, we’ll employ Groovy’s use syntax, which offers scoped runtime mixins of the processor’s methods onto instances of the PaymentType class. Consider a reimplementation of the LocalPaymentService that leverages the credit card processor as a Category.

1
2
3
4
5
6
7
8
9
10
11
12
class LocalPaymentService implements PaymentService {
 
public String process(PaymentType paymentType) {
if (paymentType instanceof CreditCardPaymentType) {
use (CreditCardPaymentProcessor) {
return paymentType.process()
}
}
// ...
}
 
}

Given this strategy, all of the logic associated with processing credit card payments is isolated to its own processor. Remote services and any future domain logic related to credit card processing is contained, and has become easily maintainable, modular, and unit testable. The other payment types benefit from the same strategy.

1
2
3
4
5
6
class CheckPaymentProcessor {
static String process(PaymentType paymentType) {
def gateway = new CheckPaymentProcessor()
// … implementation code ...
}
}

1
2
3
4
5
6
class EmployeePaymentProcessor {
static String process(PaymentType paymentType) {
def gateway = new EmployeePaymentGateway()
// … implementation code ...
}
}

1
2
3
4
5
6
class GiftCardPaymentProcessor {
static String process(PaymentType paymentType) {
def gateway = new GiftCardPaymentGateway()
// … implementation code ...
}
}

At this point, the LocalPaymentService has been reduced to a discriminator and delegator to the right Category. This is still unfortunate, because if we decide that we want to add another method of payment, we will have to modify the existing workflow for delegating to the appropriate processor. We can further leverage Groovy’s dynamic nature to resolve the appropriate payment processor Category class through a registrar. Each payment processor Category class will be resolved through the registrar as the appropriate handler for a given type. To facilitate this flexibility, we’ll again utilize a Groovy Category.

1
2
3
4
5
6
7
8
9
10
11
12
class PaymentTypeHandlerRegistrationCategory {
static final registry = [:]
 
// type => handler registration
static def register(Class type, Class handler) {
registry[type] = handler
}
 
static def getHandler(Class type) {
registry[type]
}
}

Now within an initialization block in the LocalPaymentService we can register the handlers for each type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LocalPaymentService implements PaymentService {
static {
use (PaymentTypeHandlerRegistrationCategory) {
CreditCardPaymentType.register(CreditCardPaymentProcessor)
GiftCardPaymentType.register(GiftCardPaymentProcessor)
EmployeePaymentType.register(EmployeePaymentProcessor)
CheckPaymentType.register(CheckPaymentProcessor)
}
}
 
public String process(PaymentType paymentType) {
// ...
}
}

As new payment types are required, they need only register their handler Category through the LocalPaymentService‘s initialization block. In the LocalPaymentService‘s process method, we’ll leverage the same PaymentTypeHandlerRegistrationCategory to resolve the appropriate processor Category to use as a handler for the provided PaymentType.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LocalPaymentService implements PaymentService {
static {
use (PaymentTypeHandlerRegistrationCategory) {
CreditCardPaymentType.register(CreditCardPaymentProcessor)
GiftCardPaymentType.register(GiftCardPaymentProcessor)
EmployeePaymentType.register(EmployeePaymentProcessor)
CheckPaymentType.register(CheckPaymentProcessor)
}
}
 
public String process(PaymentType paymentType) {
def processorCategory
use (PaymentTypeHandlerRegistrationCategory) {
processorCategory = paymentType.class.getHandler()
}
 
// No processor for the payment type
if (!processorCategory) throw new RuntimeException("No handler registered for ${paymentType.class}")
 
use (processorCategory) {
paymentType.process()
}
}
}

Programming for unforeseeable requirements is the premise of good application architecture. Here we’ve demonstrated leveraging Groovy’s dynamic constructs — specifically the use of Categories — to redefine a more traditional service layer implementation by making it more adaptable to evolving requirements. This dynamic and adaptable programming style offers us much more maintainable, testable, and modular code, and Groovy is such a great facilitator of those benefits once you employ its capabilities.

Dan Woods is a Senior Consultant at Object Partners. He will be presenting at this year’s Gr8Conf and SpringOne 2GX conferences on topics related to Application Architecture, a subject for which he is deeply passionate.

About the Author

Object Partners profile.

One thought on “Redefining the Service Layer with Groovy Categories

  1. Kim A. Betti says:

    Interesting, but is this really a step forward from registering processors with something like a PaymentProcessorRepository?

    1. Dan Woods says:

      You can think of the registry in the same manner as you would a Repository or a Java EE Producer Factory; it is arbitrary to how it’s referred in the end.

      But your point is well taken, and registering processors is ostensibly what this post is attempting to demonstrate. Granted, the contrived nature of the scenario obscures some of the complexities that you would find when implementing a similar solution in production, but my hope is that I’ve demonstrated how you can leverage the built-in constructs of Groovy to help build a maintainable and fluent architecture for registering and employing service-layer business components.

      Trying to demonstrate those concepts through a scenario-based approach, I feel, gives better insight into how you might actually use them, but does ultimately deviate from a “real world” scenario. In a real world scenario, we would be able to leverage a Spring container or JNDI to lookup and dynamically register our processors, but the reality is that trying to demonstrate that in a blog post can quickly become cumbersome. (Indeed, I started down that path with this post, and ultimately shortened it).

      I hope that you and others can appreciate the simplicity of demonstrating the concepts without getting lost in the details. That said, let me know if a more practical scenario is needed and I’ll happily refine the post.

      Thank you for the comment.

  2. Roberto Guerra says:

    BTW, you can use a switch statement similar to Scala’s pattern matching to replace the long if-else block:

    switch(paymentType){

    case CreditCardPaymentType:
    case GiftCardPaymentType:
    case CheckPaymentType:
    case EmployeePaymentType:
    return doProcess(paymentType)
    default:
    throw new IllegalArgumentException(“Supplied payment type is not supported.”)
    }

Leave a Reply to Roberto Guerra Cancel reply

Your email address will not be published.

Related Blog Posts
Natively Compiled Java on Google App Engine
Google App Engine is a platform-as-a-service product that is marketed as a way to get your applications into the cloud without necessarily knowing all of the infrastructure bits and pieces to do so. Google App […]
Building Better Data Visualization Experiences: Part 2 of 2
If you don't have a Ph.D. in data science, the raw data might be difficult to comprehend. This is where data visualization comes in.
Unleashing Feature Flags onto Kafka Consumers
Feature flags are a tool to strategically enable or disable functionality at runtime. They are often used to drive different user experiences but can also be useful in real-time data systems. In this post, we’ll […]
A security model for developers
Software security is more important than ever, but developing secure applications is more confusing than ever. TLS, mTLS, RBAC, SAML, OAUTH, OWASP, GDPR, SASL, RSA, JWT, cookie, attack vector, DDoS, firewall, VPN, security groups, exploit, […]