Different considerations for the project are described below, and they're a good starting point to understand the goal and nature of the project. This includes the planned product, architecture considerations, principles, potential libraries and a rough idea about the API.
Internal quality in Software Development is cheaper in the long run [1]. This means that a high effort will be put in having a good foundation for the project, so that it's possible to iterate faster, cheaper and safer. The project embraces:
- Unit Testing
- Continuous Integration/Deployment
- Metrics
- Refactoring
The final version of Perfecty Push will extend the current capabilities offered by the plugin version. The development will be iterative and in general, it should:
- Support both Push API and Websockets notifications.
- Have a Push Engine independent of the notification mechanism.
- Support user segmentation (device, location, engagement).
- Optional user presence indicators.
- Single binary and multiplatform: Windows/Linux.
- Exponential Backoff for third-party integrations.
- Unified JS SDK.
- Administration UI.
- Extendable using the Go plugin system
A potential MVP is the version that supports the same features as the plugin version, with basic user segmentation support, completely distributable in the WordPress plugin marketplace, and stable. This means:
- Supports Push API initially
- Single binary and multiplatform
- Basic user segmentation (Excluding engagements and meta attributes)
- Unified JS SDK
In this Go project there are two possible paths, which will be tried both, in phases:
- A lean Go architecture with a flat structure
- A more complex project focused on the domain and the maintainability:
- Domain Driven Design
- Hexagonal Architecture
- Clean architecture
The first approach is more common in the Go/Elixir communities, while the second comes from the Java/.NET world for enterprise focused applications. There have been some intentions to apply the latter in the former, and some strong opinions from the community against that, so it's a good experiment for this project, in case we consider it's worth it:
"The purpose of a good architecture is to defer decisions", [*]
The key here is to embrace unit testing and refactoring.
According to Clean Architecture, to guarantee a loosely coupled system we need to correctly apply the Dependency Inversion Principle and separation of concerns. A good initial architecture that could enable us to apply TDD and DDD in a future refactoring is:
- Adapters -
/internal/handlers/
and/internal/repository/
: HTTP handlers and DB repositories - Application -
/internal/application/
: Use cases - Domain -
/internal/domain/
: Called Entities but in Domain Driven Design it's simply the "domain" - Frameworks&Drivers - All the rest: The outer layer that glues the next inner layer (Adapters).
Follows: https://github.com/golang-standards/project-layout
For the long run, it's expected that Perfecty Push supports High Availability and Fault Tolerance. This means implementing the features that allow multinode execution, concurrency and (eventual/strong <?>) consistency.
Libraries:
- Strong consistency:
- Eventual consistency:
While Strong Consistency brings reliability, and a guarantee on the data when there are Partitions, it has an overhead in performance. In case the performance is more important, we should rely on Eventual consistency instead. Remember, we cannot have both (CAP).
It's important to support user segmentation based on multiple parameters:
- Device type
- Browser
- Location
- Timezone
- Engagement (depends on the User presence indicators described below)
- Meta attributes set through the SDK (Don't implement it in V1 yet, but have it in mind)
We need to know the user presence by:
- Measuring the time spent in the website.
- Heartbeat mechanism to know if the user is currently connected. It should be optionally enabled.
The push engine must be unaware of the notification mechanism. The specifics are implemented by the mechanisms, which take work from the Push Engine queue to use a back-pressure strategy.
Initially supported mechanisms:
This means we could later add others (APN, FCM).
It's the JS SDK's responsibility to choose between Push API or Web Sockets. Initially, Push API is tried, Web Sockets is the fallback.
The JS SDK will be based on the current implementation from the WP version. A separate project should be created and the quality must be improved.
Metrics are specially important for a fast Push Notification Server. In this case, we start from the baseline that the plugin version currently has a limit in this aspect and we need to measure the progressions in the development once we have a stable version. Use:
- Golang benchmarking
- Prometheus / InfuxDB
- OpenTelemetry
Structured logs are preferred over traditional logs. All the HTTP requests will use the same trace field populated in the context. In the case of push notifications, they will have an associated tracker field (user.id). Integration to the Opencensus telemetry framework should be considered.
The mechanisms that depend on third-party services
like Push API
or APN
or FCM
, should detect transient failures
in the communication with the third-party. Errors that are not related to dead endpoints (403/410),
should automatically increase the retry factor and stop at a maximum value.
If the maximum is reached, it's a symptom that either we have:
- Provided the wrong VAPID credentials. This happens in some services
that return
401
for endpoints that were expired, see this issue. - We have a failure in our side that we need to fix first (any miss-configuration or bug)
In those cases we don't want to re-try as it's considered a permanent failure that we need to address first.
Libraries:
endpoint | method | description |
---|---|---|
/v1/public/users |
PUT | Register user |
/v1/public/user/:uuid/preferences |
PUT | Change the preferences (opt-in/opt-out) |
/v1/public/user/:uuid/heartbeat |
PUT | Let the server know when the user is using the app |
endpoint | method | description |
---|---|---|
/v1/users |
GET | List the users |
/v1/users/:uuid |
GET | Get the user |
/v1/users/:uuid |
DELETE | Delete the user |
/v1/users/:uuid |
PUT | Update user |
/v1/users/stats |
GET | Get the users stats |
endpoint | method | description |
---|---|---|
/v1/notifications |
GET | List the notifications |
/v1/notifications |
PUT | Send a new notification |
/v1/notifications/:uuid |
GET | Get the notification |
/v1/notifications/:uuid |
DELETE | Delete the notification |
/v1/notifications/stats |
GET | Get the notifications stats |