Design Patterns at socra
Most design patterns I've learned have been self-taught, but here are a few of the big ones (some have official names, some don't).
Event-driven Architecture
Most everything that happens on the back
Most design patterns I've learned have been self-taught, but here are a few of the big ones (some have official names, some don't).
## Event-driven Architecture
Most everything that happens on the backend creates an `Event` object containing information about what just happened. For example, the `socra.created` event is triggered when a socra is created, and it contains a serialization of the object at that point in time.
Once created, events are dispatched to listeners based on event type, so any part of the app can subscribe to any event that occurs elsewhere. Django refers to this as signals, while others call it pub-sub. I developed our own version to maintain explicit control over how and when events are created.
Events trigger most AI and dependent functionality, and many listeners are triggered on the client from the backend asynchronously via WebSocket. Events are also extremely helpful for analytics because they contain a point-in-time serialization of the objects, allowing you to filter and aggregate for any metric you're interested in, provided the events are descriptive enough.
## Hierarchical/Categorical Separation of Concerns
I break up all app functionality on the backend and frontend hierarchically by a single noun at a time. Each "node" is allowed *exactly* one database table. This structure prevents us from defining too many models per directory, ensuring that each directory can have precisely one model (not including child folders).
### Example Directory Structure
- socras/ (contains everything related to socras)
- permissions/
- models.py (contains Permission model)
- reactions/
- revisions/
- models.py (contains Socra model)
- serializers.py
- urls.py
- views.py
- ...other files
This organization allows the app to be broken down infinitely as parts of the app need to be extended. It's also vital to limit the number of child nodes per parent to fewer than 20. This prevents any directory from becoming cluttered and ensures that anyone (including agents) can navigate any directory, as they only have to look at a manageable number of options.
## Polymorphism
I like to employ polymorphism in many areas. For instance, our main tree node, `Socra`, can represent several types (`socra`, `journey`, `action`, `update`, `chat`, etc.). This design enables us to have a single data model for impressions, reactions, file storage, and much more.
We also use polymorphism in our main user database table, which we've named `Entity` instead of `User`. `Entity` can also have a type (`human`, `agent`, `organization`, `group`, etc.). This design allows extreme flexibility in associating objects with an `Entity`, as we don't need to use generic foreign keys or create multiple foreign keys to multiple models. The downside is that we must be careful with data validation, which leads us to capabilities.
## Capabilities
When we retrieve an object via API, we create another object outlining the user's capabilities on that object, usually on a per-field basis. This allows different roles, types, or specific situations to dictate exactly how a user is permitted to interact with an object. The capabilities are passed to the client along with the object, enabling the client to know unambiguously which fields the user can modify.
Capabilities typically follow the form `can_XXX`, and can range from `can_change_name` to `can_add_child`. This system allows us to know precisely what a user can and cannot do at any time with any object, bridging the gap between client and server effectively.
## Monorepo
Monorepos are initially advantageous for a small team but can suffer as the number of web applications grows or when you need to segment permissions across different web apps. For this reason, we maintain a single monorepo for most of our JavaScript libraries, while each web app has its own repository.
## Abstract After Two Instances
The first time you build something, place most of the code as close to the definition as possible and don't focus too heavily on refactoring. When you're about to code something for the second time, refactor the first instance into an abstraction, then use that abstraction for the second item. This approach is immensely helpful for determining when to refactor versus just writing code.
I’m sure there are many more patterns to capture, but I’ll continue to add to this list as I remember.By Mike Morton