3

We have an existing web service which is currently modeled as a single project, where the web/service/manager/model classes have gotten a bit confused and intermingled. As a refactoring, we're pulling the pieces apart for long-term maintainability.

As we prepare, I've been assembling a list of "guidelines" to apply to the design of the Service level, and one of them is:

Service-level parameters should **only** be primitives.

Meaning with a class (pseudo-Java code):

class Account {
    public enum AccountType {
        PERSONAL, BUSINESS
    }

    public Account(String name, AccountType type) { ... }
}

Your Service method might look like:

class AccountService {
    public Result createAccount(String accountName, String accountType) {
        AccountType type = AccountType.valueOf(accountType);
        // TODO: Business-level validation for duplicate names, etc.
        Account newAccount = new Account(accountName, type);
        return accountManager.save(newAccount);
    }
}

My question is: Should that accountType parameter at the Service level be a primitive, or should it be the AccountType enum type?

Reasons for primitives:

  • The Service level can do validation if you give it a non-resolvable type
  • If you migrate types, the Service level can provide backwards-compatibility

Reasons for model types:

  • When writing Service-level tests, a bad AccountType is a compilation error
  • Case statements creating all types of Accounts would fail if you're missing one (ie. add a new AccountType and forget to write a test for it)

The question I try to ask myself is, if both a a REST API and a CLI tool are sitting on top of the Service layer, what makes them both easy to write?

So, is there a preference/guiding principle here?

2

Fundamentally, types exist to provide information about values. So, when designing an API, the general principle is fairly intuitive:

Argument and return types should not lie about the values they support.

For example, if the hypothetical service method exposes an integer argument, it should accept any integer.

If it only actually accepts 4 values, it's not actually accepting an integer, it's accepting the integer encoding of some other data type, so it really should be it's own enumerated type.

Where it gets tricky is if the accepted values cover a large range of values. As an example, if the valid values are between 20 and 100, would that be worth creating a type to express that limitation?

Here it becomes a judgement call, and I can only really offer my own rules of thumb - they've worked for me so far, but I won't make any claims of universality.

Principle 1: is this a hidden domain model?

Is this limitation specific to this method, or does this describe a range of values that map to some meaningful type of data.

For example, if the method accepts float values between 0 and 100, is this intrinsic to this service method, or is it actually modelling percent completion of some process?

I generally favor exposing domain models, as they increase the amount of help the compiler can give you, and decrease the amount of difficulty new maintainers have exploring the code.

Principle 2: how many places is this restriction relevant?

Creating a type to express the range of valid values isn't free. The actual cost varies by language (ex: Scala makes it much easier than Java), but it's nonzero.

Because of this, we need to keep in mind how much benefit we'll get out of adding this new type.

If the value is only ever used directly inside service method, it's probably not worth wrapping it in a type.

If the value is passed all over the place, it makes sense to validate it once and wrap it in a type so it's easy to eyeball when a method is working with a validated value and doesn't have to redo the validation.

Principle 3: how complicated is the method signature?

If the method signature includes runs of several parameters of the same type, it's often worth the effort to wrap them in types to enable the compiler to detect if the order gets messed up.

| improve this answer | |
2

Well what type of domains are we talking about? If the consumer is going to be java based and the definition is java based as well, then I would use an enum, because your method has a set of static constraints which are represented by the values of your enum not just an int.

If it was a web interface, like a REST api I would use either but probably lean towards using enums once again for the same reasons and because Swagger for example works great with them to.

| improve this answer | |
  • The consumer would be a REST API (written in Java, served by Spring), but there is a desire for a CLI (also written in Java) as well. It's basically Java top-to-bottom. – Craig Otis May 3 '18 at 13:36
  • That seems like the right way to go. I usually do both as well and end up using the CLI to perform the ATs. I would go with an enum all the way, if not for anything else, it will help validate the messages (schema) – MeTitus May 3 '18 at 13:38
  • Right - my only concern then is what if you move from those two cases to PERSONAL_CHECKING, PERSONAL_SAVINGS, ... and want to provide backwards-compatibility - does that then need to be duplicated in both of the CLI/REST layers? (And they need to know how to map PERSONAL to one of the others.) Since it's no longer a compile-time constraint enforced by the Service. – Craig Otis May 3 '18 at 13:41
  • 1
    You see, thats one of the problems, but an int wont carry enough info for a client to know what it means, they will always need to revert to the documentation to get the list of options. It's alwayas about tradeoffs. If you were removing an opion from an enum then that your be an issue, as you would be braking a contract, but adding a new option wont do any harm the existing options will be there still. At the end of the day an enum is serialized into the in anyways so adding wont break anyting, whereas removing will. – MeTitus May 3 '18 at 13:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.