A common question when working with Temporal Cloud: “Should I split this Activity into multiple Activities?” Or put another way: “How much can I cram into one Activity before it’s too much?”
Finding the right balance between too many Activities or too few involves weighing trade-offs.
Using more Activities means the Temporal service has to track more state changes, keep a larger Event History, and handle extra network requests. If you're self-hosting, this can put more strain on your system’s computing power and storage. When using Temporal Cloud, these concerns are simplified into consuming additional actions.
Using fewer Activities might seem better at first, but it can actually make things harder down the road. You’ll have fewer natural breakpoints to take advantage of Temporal’s built-in tools, which can make it trickier to configure Retry and Timeout Policies, keep your application state valid, debug issues, run tests, monitor performance, and refine your Workflow design over time.
While increased usage is an important consideration, there are these considerations beyond cost, as Activities also provide value to your application. Ideally, you should consider the value the Activity provides in relation to its cost in order to make the best decisions for your use case.
As a rule of thumb, Activities should be fine-grained: one operation (aka transaction) per Activity. This is a best practice that will ensure your Workflows are simple, maintainable, observable, easily versioned, and reliable. However, there are times when you may want or need to combine different kinds of work inside of a single Activity and having clarity on the reasoning behind the best practices can be helpful in weighing the pros and cons of doing so.
In this blog post, I’ll go over some of the main reasons you would split up the work going on inside of Activities which will help you uncover the rare exceptions to this best practice and help you to make wise decisions when you design your own Activities.
The TL;DR (Too Long; Didn’t Read)
This post is full of details and examples, but just in case you are pressed for time and want to get straight into the guidelines here’s a summary of the main reasons you should use more Activities instead of fewer:
You Must Use More Activities If…
- You need to run an Activity on a different Task Queue—for example, to route work to specialized hardware, enforce rate limits, or isolate network traffic.
- You require a different Retry Policy for certain operations.
- You need a distinct Timeout Policy for specific tasks.
It’s Practical to Use More Activities If…
- An Activity includes multiple database transactions that change state.
- You make multiple calls to external services that modify state.
- There’s a financial cost associated with certain remote service calls.
- You need to dynamically execute individual units of work, like in a DSL or DAG-based Workflow.
- Your business requires detailed observability into Workflow status.
You Might Prefer More Activities If…
- A more granular design makes development faster and maintenance easier by simplifying testing and debugging.
- You plan to use Temporal’s saga pattern for compensating transactions.
- Your Workflow involves concurrent execution, and you prefer Temporal’s concurrency model over traditional approaches. If you’re interested in learning more about these categories or the specific examples for each, keep reading as they will be covered in detail.
Should You Use More Activities or Fewer?
There are three main categories where you’ll want to use more Activities instead of fewer:
- Necessity – Some situations require multiple Activities.
- Practicality – Using more Activities can be more financially efficient, or improve business value, even if it’s not strictly required.
- Preference – Some choices make development easier and code more maintainable, but the decision to use more Activities in these scenarios come down to personal or team preference.
Necessity—When using Fewer Activities Won’t Work
Sometimes, using multiple Activities is required because using fewer Activities is impossible. You’re forced to split the work into pieces. Here are some common cases where breaking an Activity into multiple parts isn’t optional.
Using Separate Task Queues in your App
Sometimes you need to route work to specific Workers. For example, some Workers may have specialized hardware (they might be using high-end GPUs for AI/ML models) or they may be hosted within a different network. In these cases, where you want to make sure that work arrives at the right Worker, you need separate Task Queues, which means invoking an additional Activity. This logic also applies when enforcing rate limits on a Task Queue.
Consider the following example: imagine you’re building a payment processing system that needs to handle sensitive financial transactions. To stay compliant with security regulations like PCI-DSS, these transactions must be processed within a secure, isolated network—one that general-purpose application Workers can’t access. By using a separate Task Queue, you ensure that only Workers inside this secure environment handle payment encryption and validation, keeping customer data safe while allowing the rest of your application to run in a regular-security environment.
Fig 1.1: Using a separate task queue for high-security Activities ensures that only PCI-DSS compliant infrastructure can access the secure database, with outbound-only traffic to Temporal Cloud. While this setup requires an extra Activity in the Workflow, it keeps sensitive data protected and separates security concerns from the rest of your application.
Different Retry or Timeout Policies for Different Real-World Processes
When different parts of an Activity need separate Retry Policies, you must split them into multiple Activities. For example, if one step should retry only once while another allows unlimited retries, they require different configurations. This means separate Activities. The same applies to Timeout Policies.
Consider an online checkout Workflow. One step captures a payment, while another sends a receipt via email. If payment capture fails, the Workflow should fail immediately and prompt the customer to enter new payment details. But sending the receipt is different—it can be retried multiple times without impacting the order. It wouldn’t make sense to fail the entire Workflow just because an email didn’t send on the first attempt.
To enforce these distinct Retry Policies, you need one Activity for payment capture and another for sending the email receipt.
Fig 1.2.1: This setup combines payment capture and sending of the email receipt into one Activity. The problem? If the email fails, the entire Workflow fails—even if the payment already went through.
Fig 1.2.2: A better approach, as shown in Fig 1.2.1, splits the monolithic Activity into two separate Activities: one for capturing payment and another for sending the email receipt. This allows each Activity to have its own Retry Policy, ensuring that a failure to send an email won’t cause the entire Workflow to fail after a successful payment capture.
Practicality—When Splitting Activities Pays Off
Not every situation requires the use of additional Activities, but sometimes using more is the practical choice. Whether it’s about reducing costs, improving efficiency, or aligning with business needs, structuring your Activities can lead to smoother, more maintainable Workflows. Here are some cases where using more Activities is the best practical choice.
Not Just All-or-Nothing
Imagine you’re ordering takeout for your family. Everyone picks their meal, and you place a single order—because if one person’s meal is missing, dinner isn’t complete. This is an all-or-nothing situation: you either want all the meals or none, so your family can eat together.
Now, let’s say you order from multiple restaurants instead of one. Suddenly, coordination becomes a headache. Each restaurant is working independently, with no way to track the status of your full order. If one restaurant is late or forgets an item, there’s no easy way to fix it across all restaurants. This level of coordination is complicated, and most restaurants simply aren’t designed to handle it.
This is exactly the kind of challenge Workflows solve. Inside a Workflow, you have the tools to handle complex coordination across services—but once you enter an Activity, you lose that orchestration ability. If an Activity tries to handle multiple all-or-nothing concerns (aka a transaction) at once—like coordinating multiple restaurant orders in one step—it introduces risk. In these situations, you often end up needing the functionality that Temporal already provides at the Workflow level, but since you are combining the transactions into one Activity you have to build that kind of tooling yourself which is a huge time sink.
It is important to recognize that not all use of multiple services is an all-or-nothing situation. Going back to the restaurant metaphor: it is perfectly fine to make a phone call to four different restaurants and ask them what their hours and menu items are prior to ordering dinner. If one restaurant fails to pick up the phone it doesn’t change much to call them or all the restaurants again. When you order food from multiple restaurants and expect them to all deliver dinner at the same time you are expecting a change in the state of the world, not just the delivery of information. If one restaurant fails, the other restaurants may have already prepared the food they are responsible for.
In the same way that calling multiple restaurants at the same time does not result in an all-or-nothing scenario, calling multiple services, or using multiple databases does not automatically mean you are facing a scenario where you ought to be using more Activities. There’s no issue with reading from several databases or fetching information from several different APIs in the same Activity—and sometimes you may even need to! However, writing to multiple databases or using multiple APIs that change some remote state is a clear sign that you have an all-or-nothing situation on your hands.
A good rule of thumb: If your Activity’s work involves multiple database transactions or coordination of state changes across multiple remote services, you should be using multiple Activities to handle the orchestration of these transactions and remote service calls.
Bundling transactions in Activities can cause serious issues:
- What if one transaction succeeds but the next one fails? You’re left in a halfway-done state.
- How do you report errors back to the Workflow? Should you retry everything or just part of it?
- Debugging becomes much harder—since it’s unclear how far the Activity got before failing and this information cannot be deduced from the Workflow’s Event History. By splitting these steps into separate Activities, each one runs independently, making failures easier to track, debug, and recover from without having to rebuild the functionality that is already available in a Temporal Workflow.
Isolating Retries
While Activities have a small cost—fractions of a cent on Temporal Cloud—retries can sometimes trigger much higher costs in unexpected ways. There’s a real financial cost whenever a retry involves a remote service call that is charged for based on usage. In these scenarios, using more Activities allows for a distinction between Retry Policies, which makes financial sense.
Consider the example of pairing an expensive API call, like pulling a credit report, with a cheaper one like sending a customer a notification. Failures in sending a notification should not result in a potential retry on the expensive operation of fetching a credit report. Activities do have costs associated with them but you should never lose sight of the costs and labor involved with the entire Activity execution. If you have to re-run expensive steps, it’s smarter to split those steps into separate Activities that don’t have to repeat in lock-step with lower cost ones.
Fig 2.1.1: Pulling a credit report is costly and rate-limited, while updating a risk profile in your application database, and sending a notification are virtually free. But in this setup, all three tasks are bundled into one Activity, meaning a retry due to a failed notification or failure to update the database could unnecessarily trigger another credit pull. Even though these tasks seem related, there’s no need to keep them tied together. A better design would eliminate the potential of the Workflow to make duplicate calls to the credit bureau because of failures in unrelated services.
Fig 2.1.2: Compared to the design in Fig 2.1.1, this approach is much safer. By isolating the credit report fetch into its own Activity, retries for low-cost tasks—like updating a risk profile or sending notifications—won’t accidentally trigger costly extra credit pulls.
Discrete Invocation
Sometimes, you need to invoke work dynamically as separate units. This is common in DSLs (Domain-Specific Languages) and DAG (Directed Acyclic Graph) orchestration, where dynamic inputs control the flow of execution.
Temporal already supports dynamic invocation through Activities. Trying to replicate this functionality inside of an Activity would be far more complex and costly than simply structuring your Workflow with additional Activity calls.
Improved Observability
Temporal Workflows aren’t just for engineers—stakeholders like support staff and product managers also rely on them for visibility. While you could build a separate observability system, it’s often unnecessary. If Temporal’s built-in observability meets your needs, using it saves time, effort, and cost. Instead of reinventing the wheel, take advantage of what’s already there.
Stakeholders outside of your engineering team aren’t the only beneficiaries. Having excellent observability tools is a win for you and your team too. By using additional Activities you can make your application easier to debug and simpler to explain and understand.
Fig 2.3: Temporal’s built-in observability tools provide detailed insights into a workflow’s status. These visualizations make it easier to collaborate with non-technical stakeholders, keeping everyone on the same page.
Preference—Improving Software Quality and Developer Experience
Some design choices aren’t strictly required or of objective financial benefit, but they can still provide long-term subjective benefits. Using additional Activities can improve code clarity, maintainability, and developer experience. Even if fewer Activities could handle all the work, breaking things up can make debugging easier, reduce cognitive load, and help teams collaborate more effectively.
While there’s always a cost to adding more Activities, the right balance depends on how your team works and what makes your system easier to maintain over time. Here are some cases where structuring Activities for qualitative benefits is the better choice.
Reduced Cognitive load
Smaller Activities make Workflows easier to understand, debug, and modify. Breaking down work into clear, focused units reduces cognitive load, helping developers reason about each step without unnecessary complexity.
Smaller Activities also improve flexibility. They make it easier to move components, write targeted unit tests, and work with simpler abstractions. Instead of managing a large, monolithic Activity, developers can focus on well-defined, modular pieces that are easier to maintain and adapt over time.
Simpler Compensations
Temporal makes the saga pattern easy to use, especially when each local transaction has its own Activity Definition. Keeping Activities small keeps compensations clear and manageable.
Larger Activities, on the other hand, require more complex compensations, making them harder to maintain. By breaking work into smaller Activities, you create more granular, maintainable compensations that are easier to debug and reason about.
@Override
public void bookVacation(BookingInfo info) {
Saga saga = new Saga(new Saga.Options.Builder().build());
try {
saga.addCompensation(activities::cancelHotel, info.getClientId());
activities.bookHotel(info);
saga.addCompensation(activities::cancelFlight, info.getClientId());
activities.bookFlight(info);
saga.addCompensation(activities::cancelExcursion,
info.getClientId());
activities.bookExcursion(info);
} catch (TemporalFailure e) {
saga.compensate();
throw e;
}
}
Fig 3.1: Example saga compensations in Java using Temporal Activities for each individual step.
Easier Concurrency
Temporal's idiomatic code makes it natural for developers to create highly concurrent and reliable workflows. This is because high-level primitives like Workflows allow applications to use Activities and Child Workflows to break up large processes into discrete units of work and durably execute them in a concurrent fashion. By splitting work into multiple Activities your applications can run them concurrently with little additional effort or hidden complexity.
Without Temporal, performing concurrent operations like fan-out/fan-in requires the use of traditional concepts such as threads or asynchronous libraries that abstract the difficulties involved with concurrent programming inside of a single process. If you do take this route then you will need to consider the complexities of handling potential race conditions, thread pool exhaustion, durability, and all the other challenges that come along with the language-native primitives. It is totally possible to combine the use of Temporal and the use of traditional concurrent programming methods inside of your Activities–but it is often a more complex solution which requires more diligence and maintenance on your part.
public static class MultiGreetingWorkflowImpl implements MultiGreetingWorkflow {
private final GreetingActivities activities =
Workflow.newActivityStub(
GreetingActivities.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(
Duration.ofSeconds(2)).build());
@Override
public List<String> getGreetings(List<String> names) {
List<String> results = new ArrayList();
List<Promise<String>> promiseList = new ArrayList<>();
if (names != null) {
for (String name : names) {
promiseList.add(Async.function(activities::composeGreeting, "Hello", name));
}
// Invoke all activities in parallel. Wait for all to complete
Promise.allOf(promiseList).get();
// Loop through promises and get results
for (Promise<String> promise : promiseList) {
if (promise.getFailure() == null) {
results.add(promise.get());
}
}
}
return results;
}
}
Fig 3.2: Example of durably running Activities concurrently using Temporal’s Java SDK. Each point of concurrency is a separate Activity invocation.
Wrap-up
Using more Activities instead of fewer can improve your Workflows. When you’re motivated by necessity (task queues, retry, or timeout policies), practicality (isolating retries, observability, discrete operations), or preference (reducing cognitive load, maintainability, or leveraging Temporal’s concurrency model), consider using more Activities as a possible means to improve your Workflow design.
Have you found other reasons to use additional Activities or times when it is safe to combine Activities that aren’t mentioned in this post? Share your insights in the Community Slack!
You can also get started today with a free trial of Temporal Cloud and $1,000 in credits.