This is a response to Bill Fisher regarding experience with Flux:
@abdullin Also, can you clarify what you mean by "solution structure"? I am thinking about revising the examples soon.
Currently all flux samples (that I've seen) group files into folders based on technical similarity. For example, stores go with stores, action creators reside in the same folder shared with the other action creators.
This pattern works quite well for smaller projects. It feels especially good for the sample projects of various MVC frameworks, when you have just a bunch of controllers, models and views.
However, as we discovered on some production projects, such approach doesn't scale well. At some point you end up with dozens of technically similar files per folder and logically messy solution.
- Doesn't scale well.
- While you work on a component, you have to suffer from extra context switching overhead (jumping between various folders).
- Solutions become more "entangled" than needed.
- More complex IDE features are needed to support the workflow (e.g. context-aware navigation and completion).
- More merge conflicts, since it is harder to bound work by a feature (and communicate that).
We discovered that aligning solution with the domain model leads to a better design in the long run. E.g. we try to group files by their functionality or feature. This would mean, that different technical elements of a single feature could be squashed into one folder or onto one file. For example, for Flux we are considering to have a single folder for "News feed", which would contain all related action creators, fetchers and stores. Of course, there could be some other components that the this chat page would use (e.g. avatars, like buttons or user stores), such components would reside in their own folders. On the overall, such component decomposition is requires more effort than simply grouping files by class type. However, we value both the process (it leads to a deeper insight) and the outcome (solution that is more simple to think and reason about).
Of course, this is just something that seems to work only in a subset of cases I've been exposed to. There can easily be a deeper pattern which I fail to recognize.
Bill, thank you very much for your detailed answer. That is very valuable.
We try to have a solution structure that is aligned with our design, just like the rest of the code. This perspective is similar to the principles of Domain-Driven Design by Eric Evans. The implementation comes with a set of tradeoffs:
With these trade-offs in mind, in our Flux solution we plan to:
Could this work out in Flux, Bill?
Long answer - reasoning
I'll try to start the answer by showing similarities between event-driven backend design and design elements we discovered so far in Flux architecture. Some of these similarities may be very superficial, I hope you would point out such cases.
In short. Breaking down the entire system into a bunch of decomposed modules which are designed to work together - worked great for us in the past. Making them event-driven provided a lot of benefits on top of that. This is the reason why we try to reuse the patterns in frontend design as well.
Actions -> Events
Events are the most important design element: named DTOs which describe outcome of something that has happened in the past. They are carefully crafted to reflect meaningful outcomes of some real-world processes.
Events are published to all interested subscribers across the entire system, telling about something that has happened. They are generally named in the past tense to emphasize that:
UserRegistered
,PaymentProcessed
,MessageSent
(you can't change the past, you just have to deal with it). Event declarations are put into a shared package which acts as a common place for finding them (DDD term for that would be Published Language).Flux actions seem to have similar semantic. They are published to multiple subscribers and reflect the outcome of something that has happened in the past (
USER_FETCH_SUCCESS
,MESSAGE_SEND_FAIL
etc). Hence we are going to keep them in a single place.Action creators -> Request Handlers
API Request handlers in our backend are one of the places, where some branching logic can reside (they can also be known as command handlers in CQRS). Request handlers can talk to the database, other request handlers or even 3rd party services to perform their job. As a part of that process they could publish events telling about something that has happened in the past.
These request handlers are grouped logically in modules. This grouping is a result of multiple design iterations (this is easier and faster than it sounds), helping us to intuitively find them and reason about them.
Flux action creators seem to be the equivalent of request handlers. They represent a behavior, could interact with some API and publish actions telling about the outcomes of the process. We will probably try to group them in modules, according to their functionality.
Stores -> Denormalizers
Event denormalizers are classes which subscribe to events and project them to some local state, specific to the module. This state can be used to present information for display or to make some decisions.
Event denormalizers (with their state) are purely event-driven state machines. This makes them very easy to reason about.
We think that Stores are an equivalent of denormalizers in the Flux architecture. They, subscribe to events (and that is the only way to change their state) and generally have only getters that are publicly accessible.
We consider grouping Stores together with related React components and action creators into modules.
Unidirectional flow
We have a unidirectional flow of data in our system, too. It is driven by events and gives us a mental framework for driving design and reasoning about the behaviors.
Modules
Module in our solution is a logical grouping of related behaviors. Modules are represented in our solution as directories in the root folder. This makes the design very visible and easy to grasp.
We can evaluate complexity of a module simply by looking at the folder, measure it and chose to decompose the most complex ones. This helps to implement the design constraint of keeping the entire solution relatively simple. And that helps us (forces, to be more precise) to learn more about the domain.
We don't share much code between the modules (at least on the initial stages of the project). This means that we may have some duplication between them (e.g. somewhat similar handling of
MemberBlocked
event in almost all modules).However we all agree, that some duplication allows us to have a better decoupling and decomposition in the design. This pays off very well.
If we ported that approach to the Flux, that would mean that Stores and action creators would be grouped into the modules, becoming their implementation detail. Modules would be represented as folders. Actions would be stored in the single module.
Testing
We don't test implementation details much. Instead, we test module behaviors, expressed through their contracts. Module contracts in this case are backend-specific:
Since we have a unidirectional data flow, all module behaviors can be expressed in the form of use-cases for both queries and change requests:
This use-cases are used:
Since use-cases capture behaviors using the module contracts (events and request/response contracts) AND since these contracts change infrequently, our use-cases aren't very fragile.
We think, that it might be possible to describe Flux modules in somewhat similar way (reflecting backend use-cases to front-end):
Rendering (equivalent of backend query logic):
Behaviors (equivalent of backend change request):
I believe that these tests would allow us to get the similar benefits in Flux solution:
Summary
I tried to provide a brief overview of some artifacts of our design process and describe similarities that we see with the Flux architecture.
Of course, the application domains are quite different (backend vs front-end), but we hope to bring a similar design approach and reuse some concepts in our solution. There are simply too many benefits of that. We'll see how it works out (and if it even works out).
It is great that Flux architecture supports various design approaches within one paradigm. Besides, various components implemented for Flux (e.g. isomorphic components from Yahoo) can also be used in projects with different approaches. For that we need to thank the people behind Flux :)
Questions
Bill, based on your experience with Facebook Flux, what problems do you see with our approach?