Skip to main content

Overview

Building a distributed application with Reboot is meant to feel a lot like building an application using simple in-memory objects: you define data types and methods on those data types that you can call.

In Reboot, those data types are actually durable state and the methods are of a specific kind, either a reader, writer, transaction, or workflow.

Reboot applications are built by composing together calls to methods of your durable state data types, and Reboot ensures the safe composition of those methods by only allowing compatible methods to be called from different contexts. See here for more details about the different methods kinds.

In the rest of this section we discuss the fundamental primitives that make Reboot applications simpler to write and maintain.

Durability

The state data types you describe in your API are durably persisted to disk, and automatically scaled across as many cores (and machines) as are available.

That means that Reboot applications don't need separate OLTP databases, ORMs, database clients, etc. And you can easily import and export your state from or to another database.

Reactivity

Any reader method in Reboot may be called reactively, which will cause its response to be streamed to the client as it changes.

In fact, when a method is called reactively, all of the methods that it calls will also be called reactively. This provides transitive reactivity throughout all of your backend!

Calls made via React frontends are reactive by default. To call reactively using an ExternalContext you can use the reactively() method.

Atomicity

One of Reboot's superpowers is that reader, writer, and transaction methods are executed atomically: they view consistent snapshots of the world, and either all or none of their changes occur when they complete.

  • A reader gets a read snapshot: it sees exactly one version of its state.
  • A writer method runs exclusively for its state, and can mutate it however it wishes, as well as scheduling tasks. If it raises an exception before it completes, the tasks are not spawned, and mutatation to the state is not committed.
  • A transaction method can atomically read and write multiple states by calling their methods.

Transactions are particularly exciting because they allow method calls to safely compose. And composition is what lets you write the straightline imperative code that we thought that we had lost when the world moved to microservices!

For example: this transaction method is making potentially distributed calls between three different states -- usually a thorny problem! But because of Reboot's guarantees, we can safely write it exactly the way that we would like to: as a composition of calls to the Withdraw and Deposit methods.

async def Transfer(
self,
context: TransactionContext,
state: Bank.State,
request: TransferRequest,
) -> TransferResponse:
from_account = Account.lookup(request.from_account_id)
to_account = Account.lookup(request.to_account_id)

await from_account.Withdraw(context, amount=request.amount)
await to_account.Deposit(context, amount=request.amount)

return TransferResponse()

For more on Reboot's method hierarchy, see the method docs.

Idempotency

Mutations made to a Reboot app can easily be made idempotent, allowing them to safely be retried.

When calling or scheduling a writer, transaction, or workflow method on some Reboot state, the idempotently() fluent method enables ensuring that exactly one call will execute, and subsequent calls will return the previously computed result.

This is a very powerful facility for frontends (enabled by default in React), external clients, and workflows -- frontends and external clients can retry if they encounter a retryable error, and scheduled workflows will automatically retry until they eventually complete.

See the idempotency docs for more information.

Benefits

Thanks to these primitives, Reboot eliminates a lot of complexity that you would usually find in a full stack application:

  • You don't need an ORM or a database client, because your data is directly and atomically modified by your backend methods.
    • This also eliminates manual locking, because concurrency control is automatically applied via method type annotations.
  • You have a single source of truth for your API from which you get type safe client and server code generated.
    • Static type checkers like mypy, Pyright, and TypeScript's compiler work with the generated code to ensure that your code runs correctly the first time.
  • Teams adopting Reboot within an organization gain powerful network effects.
    • Rather than dealing with the pain of eventual consistency, Reboot applications (even those deployed and managed by independent teams!) are strongly consistent with one another. Data remains encapsulated by each application's public interface.
  • External clients and frontends can easily call you idempotently and reactively, without additional frameworks for managing clientside state.
  • In most cases, tasks eliminate the need for a separate workflow engine or task queue like Celery, Kafka, etc. Likewise, the need for separate clusters of workers to execute those tasks goes away.
    • Integrations allow for cleanly encapsulating these interactions with the outside world.
  • Because your application's API and data management is fully defined by your Reboot interface, in many cases you will not need infrastructure-as-code systems like Helm or Pulumi.

In short: you can drop your database, drain your queue, and delete your ORM!