Extracting common functionality between phoenix applications

I currently have three phoenix applications at work that share a common requirement, that requirement is that users must be invited to the system. Here is how I am currently doing the invitation system on one of the applications:

I have two schema’s: Invitation and User, and here’s what they look like:

schema "users" do
  field :first_name, :string
  field :last_name, :string
  field :email, :string
  field :password_hash, :string
  field :password_reset_hash, :string
  belongs_to :account, Aura.Account
  many_to_many :permissions, Aura.Permission, join_through: Aura.UserPermission, on_replace: :delete
  many_to_many :surgeons, Aura.Surgeon, join_through: Aura.UserSurgeon, on_replace: :delete

  field :password, :string, virtual: true
  field :password_confirmation, :string, virtual: true

  timestamps()
end
schema "invitations" do
  field :first_name, :string
  field :last_name, :string
  field :email, :string
  field :token, :string
  belongs_to :account, Aura.Account
  many_to_many :permissions, Aura.Permission, join_through: Aura.InvitationPermission, on_replace: :delete
  many_to_many :surgeons, Aura.Surgeon, join_through: Aura.InvitationSurgeon, on_replace: :delete

  timestamps()
end

The Invitation is a struct that holds information that is required to create a user. So it holds things like the first_name, last_name, permissions, surgeons, account, etc… just like the User.

A User that has a permission to invite another User interacts with the InvitationController to create an Invitation. When that Invitation is created an email is sent to the newly invited user with a url that has a unique token that they can click to begin the creation of their User. Once they click the link and enter a password all of the information from the Invitation is used to create their User and then the Invitation is deleted.

So this implemention has:

  • User struct
  • Invitation struct
  • Ecto migrations for the above structs
  • InvitationController with:
    • [:edit, :create_user] actions that are not guarded behind authentication to allow a new user to view their invitation, enter a password and then create their user.
    • [:index, :new, :create, :delete] actions are behind authentication to allow authentication users to view, create, and delete invitations.
  • Associated html templates and views that go along with the above controller actions

My goal is to be able to have something like this that I can drop into newly created phoenix applications.

Here’s a list of thoughts/questions I currently have:

  1. Does it make sense to extract this functionality or just continue to write this funtionality in each phoenix application? Ultimately it’s just a couple ecto structs and a phoenix controller implementation.

  2. If I was to extract this functionality, how would I handle the flow of information from the extracted module’s Invitation to the application’s User? Does the extracted module just allow the application to hand over what is considered the Invitation and User? Because each application may have different data on a user.

  3. Does it make sense to even have the concept of an Invitation? I’ve been thinking that maybe when you invite a user, it just creates a User that can’t login or isn’t activated.

  4. Is it common or make sense to extract a phoenix controller out of the phoenix application and into its own module? This was my initial thought on how I would extract this funtionality. Basically extract the phoenix controller into the module, and allow the phoenix application that is implementing the module to use it in its router. I’m not sure how I would handle the flow of data related to #2 though.

3 Likes

100% yes. Duplication leads to bugs and makes your life more annoying as you have to bounce around between applications.

There is quite a bit of duplication of content between invitations and users, and will likely be hard to model cleanly between different applications. So:

Yes, and no. I would make sure that such a non-active user is clearly marked as pending-accepting-invitation (as there are possibly many reasons for a non-actived user; e.g. account deactivation) and having invitation tokens makes sense. But the Invitations can simply be a table that holds a user id and a token, with such a pending-invitation user. The shared Invitation component would then need to know of the user table and the column(s) to modify for invitation purposes.

just my 0.02 … happy hacking :slight_smile:

2 Likes

Sandi Metz:

prefer duplication over the wrong abstraction

I’m not sure whether the “type of duplication” has been clearly identified in this case. What are the odds that all these applications will always require the same invitation (and user) functionality? (Maybe, maybe not.)

So far the experience seems to express that they all have the same starting point but there really isn’t anything about whether the functionality needs to remain in sync over various applications for their entire life cycle or whether the applications need the freedom to evolve independently.

From that point of view it is also possible that the “common functionality” could be expressed as template code that is injected into the project early in its life cycle. (And even that template code will likely be refined over time - but there is no requirement to go back and force earlier installations to stay in sync - for better or worse.)

4 Likes

If you’re interested in building your apps as a series of components or microservices, you might be interested in this course from PragDave :smiley:

Check out my review of it here: Elixir for Programmers (PragDave) - #25 by AstonJ

1 Like

A function tends to be a good abstraction though. I have a feeling they are more speaking of design patterns and such?

  1. Programmer A sees duplication.
  2. Programmer A extracts duplication and gives it a name.

This creates a new abstraction. It could be a new method, or perhaps even a new class.

  1. Programmer A replaces the duplication with the new abstraction.

Ah, the code is perfect. Programmer A trots happily away.

  1. Time passes.
  2. A new requirement appears for which the current abstraction is almost perfect.
  3. Programmer B gets tasked to implement this requirement.

Programmer B feels honor-bound to retain the existing abstraction, but since isn’t exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter.

What was once a universal abstraction now behaves differently for different cases.

  1. Another new requirement arrives.

Programmer X.
Another additional parameter.
Another new conditional.
Loop until code becomes incomprehensible.

  1. You appear in the story about here, and your life takes a dramatic turn for the worse.

This can happen to functions as well.

Mindless DRY creates unnecessary coupling.

See also the Development by Slogan with DRY links in this post.

1 Like