Luca Morelli
Software engineer and analyst
Team lead

.NET software development: defining the architecture or working within an existing one.

I have defined the architecture of projects for which I held technical responsibility, such as C-06 and C-07, long-running projects I handled as the sole developer. The other recent case studies describe consulting assignments where I joined established teams and products. My scope for making decisions differs greatly between these situations, and so does the way I apply methods and patterns.

When I design the systemI choose its structure and technologies based on the problem, constraints and people who will maintain it.
When I join a product teamI understand the existing decisions, follow team conventions and contribute within my assigned scope.

Two ways of working

01

When I define the architecture

In assignments where I am responsible for the design, I define the architectural choices and take responsibility for them.

I start with requirements and constraints, then set module boundaries, choose the stack and decide how the parts communicate. From the outset, I consider who will build, test and maintain the system. I introduce a pattern when it solves a real problem or makes the application easier to evolve.

Decisions I evaluate

  • Module responsibilities and boundaries
  • Domain rules and use cases
  • Persistence and external integrations
  • Testability, releases and maintenance
02

When I join an existing product

On consulting assignments, the architecture and guidelines are often already in place.

My first job is to understand the system: conventions, dependencies, flows between services and the reasons behind earlier decisions. I analyse, develop and test the tasks assigned to me in coordination with the team. If I find a structural issue, I propose a change that fits the project and the responsibilities of those who own its architecture.

This is the situation reflected in most of the case studies on this site. The how I work page explains the scope of my different roles in more detail.

My contribution

  • Analysing each task within the wider system
  • Developing to the team's standards
  • Testing and preventing regressions
  • Proposing technical improvements when part of the system can be made better

An example: growing an application without fragmenting it

In C-06 I defined the architecture and have been evolving it since 2013, as the sole developer.

The data is of two kinds: what sits in the database, and project data written to files and managed by a dedicated library. At first the whole application was concentrated in a single block, and as the load grew this became a problem.

I did not rewrite the system or split it into many services. Together with the client and their staff we analysed the flows and identified the bottlenecks; I then separated only what justified it:

  1. project data management, moved to a separate server;
  2. long-running processes, handed to a RabbitMQ queue and to workers that process the requests;
  3. the main backend, which kept serving the database-oriented APIs and became the single entry point for the other requests (API gateway);
  4. the gradual migration of the backend code from .NET Framework to .NET;
  5. a Redis cache in project management, to avoid repeating operations that give the same result within a short time.

Over time an Android app was added to follow field activities: it required a Web API that receives raw data from the devices and a service that processes it and prepares it for later analysis.

The load on the core decreased and performance improved, with few extra components to run. Every separate component adds complexity and running costs: I introduced one only where the benefit was clear.

The migration to .NET also makes monitoring more complete: beyond the application's own code, the .NET libraries already produce traces and metrics, which can be collected with little configuration. The next step I am considering is to extend this coverage with OpenTelemetry, basing monitoring on standard tools rather than custom-built solutions.

Backend and frontend

I have worked in software development for nearly thirty years. The Microsoft ecosystem is my main reference point, alongside the web technologies each project needs.

On the backend I work mainly with .NET and ASP.NET Core: APIs, application rules, data and integrations. I have followed the platform from .NET Framework to current .NET versions, working for small and medium-sized businesses as well as in enterprise settings.

On the frontend I have extensive experience with Angular: first AngularJS, then Angular from its earliest versions. I have also used React on large projects. The product and the team determine the choice of tool; in either case, I aim to build interfaces that remain manageable as features and teams grow.

Organising code when I can make the choice

I do not start with the name of an architecture. I start with the rules the code must express and the changes it will need to support.

Where the business logic is significant, I keep the rules of the client's business separate from the technical details (databases, messaging, external APIs), following the principles of Clean Architecture. I often organise code by feature, with a Vertical Slice approach: someone changing a feature can follow its path without searching distant folders.

I use dependency injection to make dependencies explicit, and separate commands, queries and their handlers when that clarifies the use cases. These tools need restraint: extra layers and abstractions can make a simple feature harder to follow.

Microservices and event-driven communication

Separating modules within an application and deploying independent services are different decisions.

I consider microservices when functional boundaries are clear and there is a real benefit to evolving or releasing the parts independently. That autonomy has a cost: network communication, error handling, monitoring and release coordination. In other contexts, one application with well-defined modules may be more suitable.

I have worked on projects using Azure Service Bus or RabbitMQ for asynchronous communication; in existing products I have helped with integrations and with diagnosing flows. Decoupling requires care with delays, duplicate messages, errors and traceability.

Domain and testing

Important rules should be understandable to people who know the product and remain testable over time.

I apply Domain-Driven Design ideas to the extent the problem calls for: a shared language with product experts, boundaries between functional areas and business rules placed where they can be understood and tested. Not every application needs every DDD pattern.

Automated tests check behaviour and protect future changes. I use TDD when writing a test first helps pin down a rule or an edge case; in other situations I add tests during development or reproduce a bug with a test before fixing it. Tests also make it possible to verify an API before its client is ready. Where tests are required for a pull request to be approved, I follow the team's requirements. High coverage alone, however, does not guarantee good tests.

Complexity proportionate to the problem

Architecture should help a project grow, rather than become an end in itself.

A system that exchanges events does not necessarily need event sourcing: it adds work around versioning and long-term management, and I would consider it only for a concrete need.

I apply the same criterion to layers, patterns and distributed services: start with a proportionate structure and evolve it when rules, boundaries or loads justify a more elaborate solution. In projects I lead, I take responsibility for those choices; when I work on someone else's product, I help improve it within the team's responsibilities and constraints.

Software and solution architects in the cloud

Need to design an application or evolve an existing product?

Detailed CV and references available on request.