🛠️Picnic Engineering - How we work
Picnic
Posted on Jun 4, 2025
🛠️
Picnic Engineering - How we work
As a young company in an extremely competitive space with many engineering challenges, we strive to do our best to set ourselves up for success.
Our stack in a nutshell
TypeScript everywhere
React Native for our app, RxJS for state management
PostgreSQL as our primary data store
SQLite for on-device storage & caching
Our development philosophy
Most of the decisions we make in terms of architecture and tooling are driven by whether they can allow us to make small experiments often, without fear of breaking existing code.
All of our code is written in TypeScript. We lean heavily on the type safety guarantees this provides, as well as the nice developer experience improvements like smart code completion and automatic renaming. We also heavily rely on zod for runtime type checking, mainly to ensure that any persisted data remains backwards compatible, and we share these types between the client and the server.
By only using a single language, we can truly lean into its more sophisticated features, and can ensure that no developer is siloed by their language of choice: every engineer can touch every single part of the stack with confidence.
We strongly believe that organising our code in well-defined "shearing layers" allows us to focus our efforts, and constrains our technology choices: we're aiming to build a solid, stable core that rarely needs to be touched, surrounded by layers that will change more frequently (sometimes several times in a day!). For the core, we choose technologies that are "boring" and battle-tested, such as PostgreSQL, SQLite and Redis, but we're not afraid to try experimental new libraries and approaches for the outermost layers, since these are likely to change soon anyway.
Our approach to testing follows this structure: our core is tested comprehensively with unit and integration tests, since it is critical to the functioning of the entire system. The outer layers (eg. most of our UI code) are mostly tested manually. We'd love to automate more of this, but since we're still a tiny team and our product is still changing rapidly, we don’t aim for full test coverage.
If you’d like to read more about our reasoning behind this approach, please take a look at:
App
We believe reactive programming is the most powerful approach for writing highly dynamic and stateful UI code.
Our app is written in React Native, using RxJS for state management. Our frontend architecture is inspired by libraries such as Cycle.js, in that we explicitly represent the dataflow graph and effects using RxJS observables. Early iterations of our frontend used combinations of React’s built-in state via hooks and MobX, but we found that this made debugging issues with our app state difficult once the app got more complex. Our app state needs to reliably update in real time in response to several different data streams (eg. push notifications, WebSocket, native device events, etc.), and we found that explicitly modelling these streams as RxJS observables was a natural fit. The downside of this approach is that it is relatively verbose, and requires a good understanding of the RxJS operators, but we feel that making all dataflow 100% explicit is a real superpower.
For data fetching, initial prototypes of the app used a GraphQL API built with Apollo. However, we found that this wasn't a good match for our data model, and that a lot of the libraries in its ecosystem were very immature (especially related to caching and real-time updates). We have since replaced this with a custom API, using zod to provide the type safety guarantees that GraphQL provided. We use RSocket for pushing real-time updates to the app, which provides full reactive stream semantics over the wire. This means that we can treat remote streams in exactly the same way as any other stream in the app, using RxJS.
We use the excellent Expo libraries and services for all of our daily development, as well managing our builds and performing over-the-air updates. This allows us to release critical bug fixes and changes to features without having to go through app store reviews, which can take 24 hours or more.
Server
Our server-side architecture is highly modular, and inspired by event sourcing and CQRS techniques. This allows us to fearlessly evolve our APIs and schemas while remaining backwards compatible in most cases.
On the server side, we use Node.js and Express, also using TypeScript. All of our data is stored in PostgreSQL. We treat Postgres as an append-only log of events. This log is read by "log consumers", which are each responsible for updating the specific data structures, tables and indices (mostly in SQLite, some in Redis) that are needed to efficiently serve each query. As a result, most of our reads can go directly to SQLite, allowing us to respond to most queries in single-digit milliseconds.
Our server-side architecture is inspired by CQRS: we maintain a clear separation between the high-level "read" (query) and "write" (command) models, and each feature on the server exposes high-level read & write models whose backing implementations can be changed without affecting the rest of the system.
We are big fans of append-only logs as a data structure for synchronizing changes. Most of the app's data objects are modelled as pure state machines (aka. reducers) over a log of events relating to that object. This allows us to compute the exact set of changes caused by a single event, which we push to the client over RSocket and push notifications. Additionally, it allows us to perform optimistic updates to the UI even while disconnected from the server, since the state machine logic is shared between the app and the server, and can be computed in-app without having to communicate with the server.
Infrastructure
We're still at the beginning of our journey, but we have ambitious goals that can only be achieved with stable infrastructure.
In our earliest stages, we used Heroku for all of our infrastructure, so we could focus 100% on building our product. However, we started bumping against some annoying limitations of the platform, so all of our infrastructure now lives on AWS. We use Pulumi to describe our infrastructure using TypeScript code. We use ECS to run our services, and use RDS to run Postgres. We use Datadog to aggregate logs and metrics.
For now, we are still mostly focused on getting our product to market, but we'll likely have very interesting challenges on the infrastructure side once we've launched.
Process
We aim to keep most weeks free of meetings by aligning our work to a monthly cadence.
In past lives, we've been burned by endless sprints with badly defined objectives, so instead we've developed a process which is inspired by Basecamp's Shape Up process. We align all of our feature work to a monthly cadence: 6-week cycles, followed by 1 week of "cool-down". The cool-down weeks give us to time to do up-front planning, prioritisation and design for the work we want to do next.
In cool-down weeks, we also try to tie up all the loose ends (bug fixes, refactoring, "nice-to-haves"...) which in a "normal" agile process would have to be crammed into ad-hoc "bug-smash weeks", "feature freezes" etc, which tend to be extremely difficult to get buy-in for.
Since most of our prioritisation and planning happens in cool-down weeks, this allows us to keep most weeks free of all meetings except for a quick daily stand-up and a retrospective on Friday.
If any of this sounds interesting to you, or you have any ideas for how we could improve, we'd love to hear from you. Check out our available roles at Careers at Picnic, or email jobs+dev@picnic.ventures.