Architecture Decisions
Important architecture decisions and rationales
Microservices per capability
Decision: Split the system into microservices rather than a monolith.
Rationale: Independent scaling, separation of concerns.
GraphQL for schema definition
Decision: Use GraphQL as the schema language for projection and CRUD types.
Rationale: Using GraphQL schemas as the single source to define projections and CRUD types is convenient and keeps developer experience high: you can run larger projects without writing any database migrations, and all APIs are auto-generated from the schema. Schema-driven deployment via the Deployments service keeps everything in sync.
Event sourcing
Decision: Use event sourcing.
Rationale: Audit trail, replay, multiple read models.
Sync as shared locking service
Decision: Provide a dedicated Sync service for distributed locking; Sync uses an in-memory approach and is not horizontally scalable yet.
Rationale: The goal was a solution that is extremely performant. Alternatives such as etcd or Consul can be used for distributed locking, but they were evaluated as harder to set up and slower than the in-memory approach. Pessimistic locking with hierarchy and multi-tenancy avoids each service implementing its own locking. Horizontal scalability for Sync may be addressed later.
PostgreSQL as primary datastore
Decision: Use PostgreSQL for events, projections, CRUD, auth, and deployments.
Rationale: Postgres is a standard database that is easy to self-host; backups are well documented and straightforward. Compared to Redis, Kafka, or similar, a single Postgres instance is easier to set up and operate.
Elasticsearch for Projections search
Decision: Projections support optional Elasticsearch for full-text and geo search.
Rationale: Alternatives as Typesense were evaluated, but they were not stable enough. Elasticsearch is easy to set up and operates stable in HA mode. Typesense crashed too often.