When an application needs to perform several steps for the same request, putting everything into one large method can quickly make the code hard to understand and maintain.
The Pipeline Pattern is a simple way to solve this problem. It breaks the overall process into smaller steps, with each step responsible for one specific task.
A good real-world example is an online payment API.
The source code be downloaded from GitRepo.
Imagine a customer makes a payment. Before the payment can be completed, the application may need to go through several checks and actions:
Validate the payment request.
Check if the customer is allowed to make the payment.
Check the account balance.
Run fraud checks.
Process the payment.
Save the transaction details.
Send a notification to the customer.
Instead of putting all this logic into one controller or service method, we can create a pipeline and give each step its own responsibility.
Creating the Pipeline Step Interface
public interface IPipelineStep<T>
{
int Order { get; }
Task<T> ExecuteAsync(T context);
}
Each step implements the interface:
public class ValidatePaymentStep : IPipelineStep<PaymentContext>
{
public int Order => 1;
public Task<PaymentContext> ExecuteAsync(PaymentContext context)
{
Console.WriteLine("1. Validating payment...");
if (string.IsNullOrWhiteSpace(context.CustomerId))
throw new ArgumentException("CustomerId is required.");
if (context.Amount <= 0)
throw new ArgumentException("Payment amount must be greater than zero.");
if (string.IsNullOrWhiteSpace(context.Currency))
throw new ArgumentException("Currency is required.");
return Task.FromResult(context);
}
}
The pipeline executes these steps in sequence:
Payment Request
|
v
[Validate]
|
v
[Authorization]
|
v
[Fraud Check]
|
v
[Process Payment]
|
v
[Save Transaction]
|
v
[Send Notification]
This makes the code easier to understand, test, and maintain. It also makes it easier to add, remove, or change individual steps without affecting the rest of the process.
This approach works especially well in .NET because dependency injection makes it easy to register and connect different pipeline steps.
One of the main benefits is separation of concerns. Each step has a clear responsibility. For example, if we later need to add a customer credit-limit check, we can simply add a new pipeline step without changing the existing payment logic.
The Pipeline Pattern can be useful in many scenarios, such as payment processing, order processing, message processing, validation workflows, and API request processing.
Instead of creating one large service that handles everything, we can break the process into small and focused steps:
Request → Validate → Authorize → Process → Save → Notify
This keeps the code simple and makes the application easier to test, change, and maintain.
5 Essential Things to Consider in a Sequential Pipeline
Enforce Sequential Execution
Use foreach + await so the next step starts only after the current step completes.
foreach (var step in steps.OrderBy(x => x.Order))
{
context = await step.ExecuteAsync(context);
}
Define Explicit Step Ordering
Don't depend only on DI registration order. Give each step an explicit Order so the business flow is clear and predictable.
Understand Step Dependencies
If Step 2 depends on the result of Step 1, the pipeline must remain sequential. Avoid Task.WhenAll for dependent operations.
Handle Failures and Side Effects
Decide what happens when a step fails. For database updates, payments, or messaging, consider retry, rollback/compensation, and idempotency.
Keep Steps Small and Testable
Each step should have one responsibility. This makes individual steps easier to test, replace, and extend without changing the entire pipeline.
Note
The most important design decision in a pipeline is not how many steps you have—it is understanding the dependency between those steps. When one step depends on the previous step, use explicit ordering and sequential await execution. Parallel execution should only be introduced when the operations are genuinely independent*.*
Conclusion
The Pipeline Pattern is a simple and clean way to break a complex business process into smaller steps. Each step focuses on one specific task, which makes the code easier to understand and maintain.
When one step depends on the previous step, we can run the pipeline sequentially using proper ordering and await. This helps ensure that each step finishes before the next one starts.
As the application grows, we can easily add or change individual steps without affecting the entire workflow. We also need to think about error handling, dependencies, side effects, and testing when designing the pipeline.
With .NET and dependency injection, building this type of pipeline becomes straightforward, flexible, and easy to extend as business requirements change.
Happy Coding!