Should we be creating many contexts to avoid populating large amounts of functions in them?

I have been trying to understand context and still a bit confused. I created a scaffold using the generator and tried using the same example that was given by chris.

accounts
 accounts.ex
 user.ex

I see that it dumps all the functions into accounts.ex. If I was to add another module into the accounts context am I suppose to add those functions there as well? Would the accounts.ex module become a massive dumping ground of functions if I kept adding modules to the accounts context?

I have been working with Rails many years and try to abstract methods from models using service objects. Should we be creating many contexts to avoid populating large amounts of functions in them?

2 Likes

With a Rails background the article Bring clarity to your monolith with Bounded Contexts may be more informative than any output any generator may create.

There is no code metric like β€œideal number of functions per module” that defines what a good context is. The ideal boundaries have to come from your understanding of the β€œbusiness problems” that your application deals with.

Contexts partition the application into distinct areas of your problem domain. Functionality within the same context shares the most detailed information in order to achieve the focused objective of that particular area (high cohesion, high coupling) - while at the same time only sharing the most essential information with other contexts to avoid them becoming dependent on details that may need to change in the future (low coupling).

10 Likes

Thanks for that link. It’s one of the more practical and immediately useful examples of DDD applied to Rails that I’ve seen.

2 Likes

I’ve started breaking up my contexts into separate files for organization. EG: MyApp.Inventory.Parts and MyApp.Inventory.Suppliers. You can import the files as required.

The context files do not have to be 1:1 with schemas.

That article on Rails Bounded Contexts is good, but I’m not sure I’m a fan of completely flattening the app structure. I like the controllers living in their own world and thinking of the context being the combination of model + business logic.

I can’t think of an example of where a controller could reference business logic from multiple contexts, but it feels like it could happen. Perhaps that would be a sign that you need to refactor.

1 Like

I’ve used smaller targeted service modules before trying out context approach and can confirm that contexts uniting public APIs of several such modules (like accounts) can get pretty big.

What I do is only add defdelegate calls and documentation to the context module and do the actual work in the (basically same) service modules under this context, these service modules have as much private functions as possible and only expose the required minimum.

Not sure how idiomatic this approach is, but it helps dealing with module size without resorting to unnecessary splitting and feels quite clean to me.

4 Likes

What I do is this:

User -> Users
Work -> Works
Article -> Articles

I simply cannot understand why it should be different than this.

2 Likes

If that works (and it might) then I suspect you are dealing with a (by and large) CRUD application - i.e. a situation that frankly β€œbounded contexts” were not meant to manage. It is always important to remember that bounded contexts don’t always apply and that there is a place for Smart UIs - the problem is that Smart UIs have a limited growth potential should the domain evolve and become more complex (which may never happen).

I would agree that in general controllers are simply part of the web UI which in effect simply renders a representation of the relevant information found in the associated context - so as such it would make sense to keep them outside of the β€œbounded context”. I also think that the article simply captures their own understanding of their problem domain (and DDD) at the time of writing and the article may look quite different if it was written now.

There seems to be the expectation that one should be able to get the contexts β€œright” on the first try. Eric Evans actually paints quite a different picture:

Often, though, continuous refactoring prepares the way for something less orderly. Each refinement of code and model gives developers a clearer view. This clarity creates the potential for a breakthrough of insights. A rush of change leads to a model that corresponds on a deeper level to the realities and priorities of the users. Versatility and explanatory power suddenly increase even as complexity evaporates.

This sort of breakthrough is not a technique; it is an event. The challenge lies in recognizing what is happening and deciding how to deal with it.

(Domain-Driven Design: Tackling Complexity in the Heart of Software p. 193)

So in the end identifying contexts is quite a β€œfuzzy” activity - at least from a technology perspective.

5 Likes

At Novistore we’re creating a fairly large e-commerce platform and so far creating many contexts works for us the best.

Aside from this the project is separated into 3 apps

apps
β”œβ”€β”€ novistore
β”œβ”€β”€ novistore_api
└── novistore_web
  • novistore - business logic layer (below structure)
  • novistore_api - GraphQL layer
  • novistore_web - Phoenix layer

Current structure

novistore
β”œβ”€β”€ accounts
β”‚   β”œβ”€β”€ accounts.ex
β”‚   β”œβ”€β”€ address.ex
β”‚   β”œβ”€β”€ customer.ex
β”‚   β”œβ”€β”€ organization.ex
β”‚   β”œβ”€β”€ user.ex
β”‚   └── user_permissions.ex
β”œβ”€β”€ assets
β”‚   β”œβ”€β”€ assets.ex
β”‚   β”œβ”€β”€ domain.ex
β”‚   β”œβ”€β”€ sales_channel.ex
β”‚   β”œβ”€β”€ sales_channel_collection.ex
β”‚   └── shop.ex
β”œβ”€β”€ authentication
β”‚   β”œβ”€β”€ authentication.ex
β”‚   β”œβ”€β”€ sales_channel_context.ex
β”‚   β”œβ”€β”€ sales_channel_session.ex
β”‚   β”œβ”€β”€ user_context.ex
β”‚   └── user_session.ex
β”œβ”€β”€ calculators
β”‚   β”œβ”€β”€ subtotals.ex
β”‚   β”œβ”€β”€ taxes.ex
β”‚   └── totals.ex
β”œβ”€β”€ catalog
β”‚   β”œβ”€β”€ catalog.ex
β”‚   β”œβ”€β”€ collection.ex
β”‚   β”œβ”€β”€ collection_product.ex
β”‚   β”œβ”€β”€ permalink.ex
β”‚   └── tag.ex
β”œβ”€β”€ common
β”‚   β”œβ”€β”€ attribute.ex
β”‚   β”œβ”€β”€ metadata.ex
β”‚   └── permissions.ex
β”œβ”€β”€ distribution
β”‚   β”œβ”€β”€ distribution.ex
β”‚   β”œβ”€β”€ invoice.ex
β”‚   β”œβ”€β”€ listener.ex
β”‚   β”œβ”€β”€ order.ex
β”‚   β”œβ”€β”€ order_line_item.ex
β”‚   └── shipment.ex
β”œβ”€β”€ merchandise
β”‚   β”œβ”€β”€ dimensions.ex
β”‚   β”œβ”€β”€ inventory.ex
β”‚   β”œβ”€β”€ merchandise.ex
β”‚   β”œβ”€β”€ product.ex
β”‚   β”œβ”€β”€ product_variant.ex
β”‚   └── product_variant_price.ex
β”œβ”€β”€ permissions
β”‚   β”œβ”€β”€ permissions.ex
β”‚   β”œβ”€β”€ sales_channel_context.ex
β”‚   └── user_context.ex
β”œβ”€β”€ providers
β”‚   β”œβ”€β”€ mollie
β”‚   β”‚   └── mollie.ex
β”‚   β”œβ”€β”€ postnl
β”‚   β”‚   └── postnl.ex
β”‚   β”œβ”€β”€ payment.ex
β”‚   β”œβ”€β”€ providers.ex
β”‚   └── shipment.ex
β”œβ”€β”€ sales
β”‚   β”œβ”€β”€ checkout.ex
β”‚   β”œβ”€β”€ checkout_line_item.ex
β”‚   β”œβ”€β”€ payment.ex
β”‚   └── sales.ex
β”œβ”€β”€ shipping
β”‚   β”œβ”€β”€ country.ex
β”‚   β”œβ”€β”€ region.ex
β”‚   β”œβ”€β”€ shipping.ex
β”‚   β”œβ”€β”€ zone.ex
β”‚   β”œβ”€β”€ zone_country.ex
β”‚   └── zone_region.ex
β”œβ”€β”€ validation
β”‚   β”œβ”€β”€ validators
β”‚   β”‚   └── validate_currency.ex
β”‚   └── validation.ex
β”œβ”€β”€ vendors
β”‚   β”œβ”€β”€ brand.ex
β”‚   β”œβ”€β”€ manufacturer.ex
β”‚   └── vendors.ex
β”œβ”€β”€ application.ex
β”œβ”€β”€ changeset.ex
β”œβ”€β”€ helpers.ex
β”œβ”€β”€ pub_sub.ex
β”œβ”€β”€ random.ex
└── repo.ex
4 Likes

@hlx What was your process like to reach that organization? So far, I’ve created large contexts with the intent to break them down as necessary.

1 Like

The contexts were growing in size and became very difficult to handle. First we tried moving it into smaller files and just use defdelegate in the main context. It worked for a while but the directory structure grew in depth and code duplication became a thing.

Example

novistore
β”œβ”€β”€ accounts
β”‚   β”œβ”€β”€ users
β”‚   β”‚   β”œβ”€β”€ user.ex    User schema
β”‚   β”‚   └── users.ex   User context
β”‚   └── accounts.ex    Account context, defdelegate create_user(params), to: Novistore.Accounts.Users

In the end we decided to move authentication out of accounts (making more contexts) and move the calculators into a separate context (so we can use it in both sales for checkouts and distribution for orders). This resulted in smaller contexts, a more flat directory structure and more pleasant to work in. Naming more contexts is a pain in the behind though, we’re still figuring out if all these names work for us but changing them is really simple because they’re all small.

I believe @michalmuskala said somewhere that contexts can even be named UserRegistration if it helps you better structure your code instead of having huge contexts.

2 Likes

I see it like this: Contexts should only be called from the outside via their public interfaces. Such a public interface doesn’t necessarily need to be one single module. It could also be multiple if they’re needed and make using that interface more sane. The problem of multiple modules I see mostly in the documentation/communication about what is public interface and what isn’t.

1 Like

Thanks for this. Exactly what I was looking for.

1 Like

This is exactly what I do as well.

1 Like

If you have to add a lot of defdelegate's you can write a quick macro that handles it for you

example: https://gist.github.com/hl/b5fff7a3a77e8ec522454949a91b9cae (untested)

3 Likes

I’m not sure I should do this since most of the module is the documentation anyway and the delegate calls are not very cumbersome. Also there is the issue with optional parameters - these are only optional in the context module, service modules receive defaults.