In the last part, we talked about why we built Vyshyvanka. Today, we're going to talk about how. Software architecture is often the difference between a project that scales and one that becomes a tangled web of technical debt after six months. For Vyshyvanka, we knew from day one that modularity wasn't an option - it was a requirement.
The Dependency Rule
If there's one rule we follow, it's the Dependency Rule: Dependencies flow strictly downward.
- Vyshyvanka.Core is our heartbeat. It's pure C#, containing only domain models, interfaces, and contracts. It has zero dependencies on anything else. This makes our core logic incredibly easy to test and reason about - if it compiles, it's logically sound.
- Vyshyvanka.Engine depends only on Core. It's where the magic happens: execution pipelines, persistence, and auth.
- Vyshyvanka.Api and Vyshyvanka.Designer sit at the top. They consume Core and Engine to provide the REST endpoints and the interactive Blazor WebAssembly UI, respectively.
By ensuring our core domain logic doesn't know the API or UI even exist, we've created a system where we can swap out a UI framework or change how an API handles serialization without touching the workflow engine itself.
The Modular Structure
Here is how we organized the solution to keep things clean:
- src/Vyshyvanka.Core/: Domain layer. This is where our entities, interfaces, and enums live. If you want to know what a 'Node' or a 'Workflow' looks like at its simplest level, this is where you go.
- src/Vyshyvanka.Engine/: This is the engine room. It houses the execution logic, persistence strategies (EF Core), and the plugin loading subsystem. It's robust, tested, and entirely infrastructure-agnostic.
- src/Vyshyvanka.Api/: Our ASP.NET Core REST API. It handles authorization, request/response DTOs, and middleware. It acts as the gatekeeper, ensuring that every request is valid and authenticated before hitting the engine.
- src/Vyshyvanka.Designer/: Our Blazor WebAssembly frontend. This is a complete SPA. It communicates with the backend exclusively through HTTP, keeping it decoupled from the engine's internal state.
Why This Matters for You
Why go through the effort of splitting a project into 6+ distinct projects?
- Testability: Because Core and Engine are so decoupled, we can write unit tests that execute complex workflows in milliseconds, without spinning up an API or a database.
- Parallel Development: A team could theoretically work on the UI components and the plugin engine at the same time without fighting over file changes.
- Resilience: If the API layer has a bug in its serialization logic, it's contained to the API. It can't accidentally corrupt your workflow engine state.
Keeping it Clean
We use Directory.Packages.props to manage dependencies across the entire solution, which prevents version drift and keeps our build times fast. Furthermore, we enforce these boundaries with code reviews and clear documentation in our structure.md.
Building a workflow engine is complex, but keeping the architecture simple is the key to maintaining it. By forcing dependencies downward, we've built a foundation that allows us to add new integrations, new triggers, and new logic nodes without ever fearing that we're breaking the core system.
In the next part, we'll look at the Domain Model - the actual building blocks of every workflow you'll ever create in Vyshyvanka. We'll break down what makes a Node a Node, and how we handle connections that span different execution states. Stay tuned!
Check out the project source code here: https://github.com/homolibere/Vyshyvanka
Top comments (0)