Last time I wrote about the data separation at the database level. As we already know each component should work as an (almost) independent service. How can that be achevied at the codebase level? What to do when for example in Accounting we need address data for an Employee? How can we get the Organization subscription plan if we're in the Checkout component?
Each of those components should work as a separate app ideally. If we imagine them as a standalone apps, it might be easier to design future code and not couple services with each other.
As in the previous blog post, let's focus on some kind-of real life example.
Marketplace
Let's say we have a simple app in which a User can register an account, select given subscription plan, put ads with their Goods and also buy Goods from other users. Depending on the subscription type, we offer them better shipping options, discounts, etc.
We can describe few components in this scenario.
app/
|- components/
|- users/
|- marketplace/
|- payments/
|- subscriptions/
Each of the components has their own controllers/, models/, services/, etc. In more complex apps we might see infrastructure/, domain/, repositories/ and others, but let's stick to simple example.
In ideal world, each of the component lives on it's own - Users know only about the Users model (and other related to Users), they check if User is logged in, update their passwords, account data, etc.
But at some point user hits a Marketplace and want to order something. In Marketplace component we want to know where the order should go, who is the recipient and what possible discounts we should offer.
Instead of doing something like this
# app/components/marketplace/controllers/orders_controller.rb
def create
@order = Marketplace::Models::Order.new(
user_id: current_user.id,
shipping_address: current_user.shipping_address,
offering: current_user.subscription_plan,
discounts: current_user.discounts,
items: order_params[:items_ids]
)
...
end
We don't want to couple Marketplace with Users. We definitely shouldn't use user relations here, as they might change in future and this code would break.
The default scope of user.discounts might change and it'd be hard to debug, why suddenly the behavior of our endpoint changed.
Instead we should use Facades or APIs or any other approach that will let us put the query to another component in one place and make sure we always get the same result.
What I mean through that is the users/ domain could expose their public API for other domains to fetch users' data.
# app/components/users/public/api.rb
class << self
UserResponse = Data.define(:shipping_address, :subscription_plan, :discounts)
def fetch_user_data(user_id:)
user = Users::Models::User.find_by(user_id:)
UserResponse.new(
shipping_address: user.shipping_address,
subscription_plan: user.subscription_plan,
discounts: user.discounts.active.sum(:discount_amount)
)
end
end
# app/components/marketplace/controllers/orders_controller.rb
def create
user_data = ::Users::Public::Api.fetch_user_data(user_id: current_user.id)
order = Marketplace::Models::Order.new(
user_id: current_user.id,
shipping_address: user_data.shipping_address,
offering: user_data.subscription_plan,
discounts: user_data.discounts,
items: order_params[:items_ids]
)
end
What this changes is that this API is the only source of truth. If every component uses Users::Public::Api to fetch User data, we don't need to worry about checking all references to users.shipping_address when changing something around users' address. The only thing we need to ensure is working as expected is the API we wrote - if it's still returning same values as before our refactor of shipping_address, then we're golden.
All user.shipping_address occurrences should happen within users/ component, all other components should access it through Users::Public::Api.
The users/ component is the owner of the code, and they know that they can't change it, break it, or do whatever that affects other domains that consume that data.
If they want to implement a change in a way how discounts are returned, then a new version of API might be implemented while the old one remains the same. Or, if everyone (all components that use that data) agree and synchronize, then the change is possible in existing API.
Why is it that important
This prevents from calling directly ActiveRecord models from different domains. Direct calls create strong coupling when it's not needed. We just need data from three columns, we don't need whole User record. What if those columns got migrated to different model? We'll try to read user.shipping_address, but it's no longer there and we need to debug. If we're using given component's API, if they migrate data to another table, they also make sure that the API returns the same values but from another data source.
For us (consumers) it doesn't matter from where the data comes from. We just want the address and the users/ should give it to us if we're calling their API. If we call their models directly, we need to know where exactly the data is - and that's not needed if we're in marketplace/.
Top comments (0)