Contexts: Accounts.User or User.Accounts?

I am still struggeling with the context ans there are not much examples to learn from. Many of the online exaples are just a copy of the Accounts context which holds Users.

However, wouldn’t it make more sense to have a User context which holds Accounts? As far I know, you can’t create a user (without adult content) but you can create an account for a user. So you get:
User.Account
User.Profile
User.Preference
Etc

Am I missing somerhing? And could you give me some links to more extensive examples of a larger system?

User is a “thing”, an entity

Example, where would a Role entity fit your User domain?

A context is probably wider and more abstract. So, maybe Accounts is a misleading term (because you take it as a part of a user), it could be Accounting, or Authentication.

I would say that contexts are abstracts containing many entities, and also that entities are not suitable to be context.

2 Likes

Not many of those exist at the moment but per many threads they will be available with more explanation at 1.3 release. Until then I’ll build my app not worrying about the right or multiple contexts. Perfection is the enemy of production and thats even when and if perfection even exists.

Here’s the thing about this kind of analysis: if you’re struggling to find a boundary or name a concept, you’ve probably got it wrong, or at least coming at it from the wrong angle.

What is the purpose of the Accounts context? What is it intended to manage? From your post, it seems like its intended to manage some amorphous concept of a user, but no real defined responsibilities beyond that.

My take: a User almost always starts out as a way to allow someone to log into a system. That set of responsibilities is more properly collected under an Authentication context, and we can immediately start to see that the important concepts there are going to be something that includes a username of sorts, and a secret or some kind of bearer token which proves identity. call that an Account, or a User—the name doesn’t matter a whole lot (though I tend to go with the latter, reserving Account for something like a Subscriptions context where the payer may not be the same as the person logging in.)

The next thing we usually care about with respect to a User is whether or not they’re allowed access to some piece of data or screen—this is an example of an Authorization context. Authorization is often modeled as a grant for an Actor to perform some action on a Resource. Later, when the Actor requests to perform the action (which may be viewing data as well as modifying application state somehow), the application asks the Authorization context whether or not the action is permitted for that Actor and Resource. (The grant may be explicit, or implied based on other features of your application.) Now the question is how to convey the User/Actor across the contexts?

The way I usually model this is that it is the application’s responsibility to do this, not the individual contexts. What this looks like in practice is that something at the application boundary (e.g. phoenix controller/channel, a plug, whatever your interface is). I also don’t pass the entire User struct across the boundaries—for the purposes of identifying an actor, its usually enough to supply an ID field of some sort. Similarly, since the stand-in for a Resource may be any notion across your system, the use of an ID is good enough. Thus, you don’t need to have duplicated models in multiple contexts.

This is obviously a pretty small modeling exercise covering the basic uses of a User. You can certainly expand from here—another concept/context I often include in an application is Profiles, managing data like user’s real name, bio, etc. While you may have a common thread running through all of these contexts—each piece of data that they all control is possibly linked by the common identity of the User, the only real link they need to share is the ID. There is no reason, for example, for an Authorization context to concern itself with the details of the user’s profile information.

The real temptation is to try to create a more concrete link with associations to make it easier to get at the data from a root object. I certainly indulge in this myself sometimes, but I can’t say I’m proud of it—its usually a mistake to do so, creating a strong coupling where none is necessary.

Hope that helps some…

11 Likes

For me, Accounts and User are very similar words and practically interchangeable, and so it’d be confusing to use them both.

Instead, I would look at how you talk about the functionality:

For example,

  • A user logs in to their account
  • A user can specify their notification preferences
  • A user uploads an avatar

If you always talk about users, then you could have modules User.Authentication, User.Notifications, User.Profile, all accessed through your User context.

vs

  • A session is created when the account is authenticated
  • The system checks notification preferences attached to the account before sending email
  • After upload the avatar is associated with the account.

Then you could have modules Account.Authentication, Account.Notifications, Account.Profile

You could substitute account/user in any of the descriptive sentences and the meaning would be unchanged. For me, that’s a sign of a context being too specific.

To continue the example, your context could have methods like

User.get_user_matching( authentication_params )
User.should_receive_notification?( user, message )
User.replace_avatar( upload, user )

The systems using the context don’t have any insight into how, say, the notification preferences are saved or checked. They could be an embedded schema, a separate table, be wrapped in a check to see if the email has been validated, or even be hard coded “true” while the functionality is being developed.

In reverse, the modules that are accessed through the context have no insight into how they’re being used. They don’t know (or need to know) if the avatar was uploaded to s3 or a local file store or if it’s a gravatar url. They just need to know how to associate the information with a user.

So uploads could be handled by an Uploads context, which has a single schema UploadedFile, and various methods to handle uploading images vs text files. The controller brings the two together, something like, success = params |> Uploads.upload_valid_avatar() |> User.replace_avatar(current_user)

The generators all create very specific crud methods, and I think it leads to the feeling that Contexts are about organizing your schemas. Contexts are about organizing functionality. Schemas are often the first step people take in designing a system - you build a blog, so you start with a BlogPost, you build a login system so you start with a User, but that’s simply a starting point.

1 Like

I think of contexts more in terms of how java packages work. It’s organizing code in a way that makes sense. Maybe it’s easier to think of it that way, along with the organization you are creating a contextual boundary.

Thanks all for the extensive answers. I hope I get this right:

  • Schemas and Controllers don’t have to align. They can have different names.
  • So you can have a schema without a ‘direct’ controller

Assume I have schema’s

  • User, which has an username and a password
  • Profile, which contains email, full_name and avatar
  • Notification, which has wants_email and wants_whatsapp

For which there is no user context at all. Cause…

I can have contexts like this

  • Authentication
    ** Authentication.Session
    *** Login(current_user_id)
    *** Logout(current_user_id)

  • Authorization
    ** Authorization.Permissions
    *** can_access(current_user_id, path)
    ** Authorization.Management
    *** grant_admin_permissions(user_id)
    *** revoke_admin_permissions(user_id)

  • Profile
    ** Profile.Avatar
    *** change_avatar(current_user_id, avatar_id)
    *** list_predefined_avatars()
    ** Profile.Username
    *** set_username(current_user_id, username)
    ** Profile.Registration
    *** is_username_availabe(username)
    ***register_user(username, password)

  • Notifications
    ** Notifications.Preferences
    *** set_email(bool)
    *** set_whatsapp(bool)
    ** Notifications.Center
    ** list_notifications(current_user_id)
    ** set_as_read(notfication_id)

Am I getting in the right direction? The controller might has to use functions from other contexts. This ain’t a problem of breaking boundaries?

ps. I know perfection is first class enemy of production. But I like to learn new paradigma’s and apply them to understand development better (as I am no ‘real dev’ :))

Just make it simple and out everything in the same context. When you start noticing that a subset of your functionality only uses a common subset of DB columns, create a new context. Until then, keep it simple.

1 Like

The same goes for Schemas and Contexts. A Context can provide access to several schemas, or no schemas. All a context is a module, after all. Unlike an OOP system, you’re not inheriting any behaviour or methods or parameters. If you’re from an OOP background, it’s similar to the Facade pattern - you’re creating an interface to access functions.

Using multiple contexts is expected, in the same way that you might use both Keyword Lists, Maps, and the Enum module. Instead of directly dealing with the internals of a Map or Keyword list, you make changes through the functions in the module. Functions that apply to both are in Enum. The code inside Enum needs to know how to handle both, but you don’t need to look under the hood.

Contexts are similar. Instead of dealing directly with the tables or sql, you access them through the appropriate module (context).

Your latest post has a very fine grained breakdown. You don’t have to break things down to that level. If the underlying functionality is very disparate, then that distance could be good. If they’re very tightly coupled, then it might be good to keep them under the same context. So, if set_email and set_username always happen together then it might be simplest to put them in the same context, and possibly to collapse them into one method (update_profile).

I can be quite perfectionistic, so I understand the desire to do things “right” the first time. But it’s far more important to be able to be -flexible-, to be able to make changes easily, than to be “right”.

One of the reasons to organize your code into contexts is that it makes it easy to change later. If today you put all the suggested calls in one Context: User, or Account, and then in a month you decide that you want to extract notifications, your process is this.

1.) Create a new Notifications module
2.) Cut and paste the code from the old context to the new context
3.) Create new notification tests files
4.) Cut and paste the notification tests into the new files
5.) Search and replace the context names in your code and text files.
6.) Run your tests.

Compared to trying to find every use of a struct or field or reviewing every sql statement, it’s a much simpler change process. Keeping change simple is one of the most important reasons to organize your code into modules. Your contexts should be organized so that if you make changes to the internals of a system, it doesn’t affect the code using the system. So for example, if you change how ‘mark_as_read’ works under the hood, it doesn’t change any of the code that calls ‘mark_as_read’.

Even though it’s geared toward object orientated code, you may want to read up on Code Smells. I find at least half of them still apply to functional code, and understanding why they can indicate a problem, and if refactoring is needed, has really improved the maintainability of the code I write.

1 Like

Is there any particular reason you made the user the first argument in should_receive_notification? and the second argument in replace_avatar? Is there a convention for this?

No real reason. Usually my convention is to use whatever order of arguments will make piping (|>) easiest.

1 Like

Well, a little off-topic but I can’t just let this go.

There is actually a convention for it: the subject to the function is the first param, that normally makes piping clearer and not easier.

For example, what probably is the @law’s case for not respecting this convention on User.replace_avatar/2:

# 1 - not respecting the convention (@law's choice)
user = Account.get_user!(id)

map
|> Map.fetch(:upload)
|> User.replace_avatar(user)

# 2 - respecting the convention
upload = Map.fetch(map, :upload)

id
|> User.get!()
|> User.replace_avatar(upload)

Well, there are exceptions for this convention IMO, but you should always ask yourself what is the subject of your function and try to respect that.

If you ask this for all the functions you do, you’ll see that the second option makes more sense: updating the user avatar is the primary objective of the code, and not just getting the :upload from a given map and transforming it in a updated user, getting the :upload is just a side task you need to do to make the main objective reachable.

3 Likes