1

I have a simple relationship where top-level items (Recipe) have a one-to-many relationship to children (Ingredient) and each item has a unique identifier (ID).

For simple CRUD operations the flow is:

  RecipeService   ->   RecipeManager   ->   Recipe

IngredientService -> IngredientManager -> Ingredient

I struggle with two primary decisions when designing service/manager classes:

First: Models vs. Primitives

Do you provide primitive values, or model objects, during insertion of an item that affects a relationship? For example:

class IngredientService {
    func addIngredientToRecipe(ID recipeID, String ingredientName, int ingredientAmount)
}

Versus:

class IngredientService {
    func addIngredientToRecipe(Ingredient i, Recipe toRecipe)
}

The first requires an ever-growing list of arguments, but handles lookups and validation internally (does the Recipe exist, how do we go get it, etc.)

The second is cleaner, but requires that callers already have looked up a Recipe, and created an Ingredient.

There's also the permutation:

class IngredientService {
    func addIngredientToRecipe(ID recipeID, Ingredient ingredient)
}

Second: Service vs. Manager for an Ancillary Type

Can services depend on each other, or should they dive straight into managers? Say we implement:

func addIngredientToRecipe(ID recipeID, String ingredientName, int ingredientAmount) {
    ...
}

When looking up the existing recipe, should we use the RecipeService, or the RecipeManager? It's safe to assume they'll both have ID-based lookups, even if the service simply shadows/forwards to the manager.

These kinds of early-on decisions feel important in establishing good patterns and styles, but I struggle to identify whether there are pitfalls one way or the other.

  • What are the purposes of your "Manager" classes and "Service" classes? What are they supposed to do? – Robert Harvey Jun 14 '18 at 22:34
  • Why isn't "AddIngredient" simply a method on your "Recipe" class? – Robert Harvey Jun 14 '18 at 22:35
  • @RobertHarvey The Service class enforces business logic (no two recipes can have the same name, for example) and interacts with the Managers, whereas the Manager is responsible for persistence (memory, database, etc.) but remains agnostic to the business/validation side of things. I didn't make addIngredient() a method on Recipe because then when persisting the parent Recipe, who is responsible for determining if the Ingredient needs to be saved, or if it's duplicated/reused from another Recipe? And what if we want Ingredient autocomplete - it seems an IngredientService would be good for that. – Craig Otis Jun 14 '18 at 22:42
  • @Craig That doesn't sound correct... a service is for data / infrastructure calls and a manager is... a wrapper class (that "manages") typically at least in what I've seen so far. Your business logic goes in factories and your data goes in services. Factories then call the services (or however you'd apply that to your design). Try to keep your services interacting together in factories creating the BI layer (it's not always possible ex. SOC vs data integrity). – RandomUs1r Jun 14 '18 at 22:52
  • are your 'managers' 'repositories' – Ewan Jun 14 '18 at 23:00
1

Super quick answer. I may expand on later.

Primitives vs models.

Use the models. You don't know what info the function will require when you specify the Method/function signature. So its best to include all the info. If you are creating an interface for example it can remain constant over many versions of a service.

Managers(repositories) and where to put them.

Don't put your repositories in your services. The service logic should be able to run idempotently, so any internal state will be problematic. This also solves your first problem with where to get the models from.

Retrieve the objects from the repository, pass them to a service or services to manipulate them and then put them back in the repository.

Confusingly that orchestration of those services as described above, may be a service or service layer itself. So the rule isn't infallible, but you get the idea.

Otherwise I think you have a good ADM style pattern there. Theres always a question over functions that modify their parameters, but a quick style change to functions returning new objects can solve that with no real change to the code if required.

  • Yea, that was my primary confusion - I get that something needs to operate on these relationships (when adding a new Ingredient to a Recipe, let's also send out an event about it!), which I figured was the Service layer. The reason I wanted the Services to also do the "getting" and "putting" of the Repository items is that the Service layer is (as I've been reading) a great place to define your @Transactional boundaries, if you're speaking Spring. – Craig Otis Jun 14 '18 at 23:39
  • So if there's a Service for idempotent behaviors (adding an Ingredient to a Recipe, sending out an event, doing validation, etc.), and a separate Service for data retrieval, use of the aforementioned Service, and then persistence of the modified data, is there common terminology for those two "Services" that have separate roles? – Craig Otis Jun 14 '18 at 23:59
  • I'm trying to think of some terminology. I guess your XXXService with pure logic is in the Business Logic Layer, while the orchestration of said services and repositories is either part of an app or the root of a hosted microservice and sits on a higher level service or application layer – Ewan Jun 15 '18 at 6:23

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.