#
Rules to Live (Code) by
Below are the principles that should inform design decisions for the Back-End team. Back-end developers are encouraged to add content below via a merge request.
#
Team Goal
To design applications that retrieve relevant data via APIs while applying validation and business logic along the way. APIs should be developed to be as robust, secure, and easy to read and maintain as possible.
#
Guiding Principles
#
General
- Gather available information. Weigh the outcome. Prioritize impact. Make decisions quickly. Keep things moving.
- Avoid over-engineering. Meet the need. Don't try to build for every possible future scenario. Instead, build systems that will be scalable (within reason) when the time comes.
- Back end: business logic; Front end: display logic.
- In discussions, ruthlessly avoid falling into the trap of the Law of Triviality.
- Commit early, push often.
#
Teamwork / Collaboration
- Track all changes in Jira tickets. There should be one ticket for every feature/bug. There should be one feature branch for every Jira ticket, resulting in one commit message per Jira ticket once ultimately merged to
main. - Every Jira ticket that defines an object should have the contract defined in the ticket. If the contract changes, the team (other developers including be & fe, dba's, testers, etc) must be notified by Jira ticket.
#
Data
- If possible, get the data directly from the source.
- If retrieval is expensive, use appropriate caching techniques.
- Use an API to retrieve data that you do not explicitly own.
#
Endpoint Design
- Design endpoints for broad applicability, then restrict access or tailor responses as needed for specific audiences—don’t build narrowly and try to generalize later.
- For a request to change an endpoint that's used in multiple places (not an endpoint for a specific app), consider:
- Is the additional complexity worth it?
- If it's a common scenario we're trying to solve, yes.
- If not, handle the code in the FE or create a new endpoint.
- Is the additional complexity worth it?
- Strive to make endpoints consistent, but also flexible enough to support diverse use cases across different business units and applications.
- Avoid "magic" behavior: endpoints should not return wildly different structures depending on input unless explicitly documented.
- Always return structured responses wrapped in a MidlandResult
by using the .ToMidlandActionResult();extension method available from theMidland.Core.Abstractionspackage in theMidland.Core.Commonnamespace.
#
Migration & Refactoring
- Favor the Rule of Three/Write Everything Twice (WET) over excessive DRY code (two instances of similar code do not necessarily need refactoring).
- Additionally, when considering refactoring for DRYness, consider not just the similarity in the code but the purpose of each class and whether they represent the same underlying concept.
- Consider the Boy Scout Rule: leave the code you interact with better than you found it, but refactoring an entire app when you originally set out to make a small change is probably not productive.
- When updating older webforms to make connections to back-end APIs for new development, consider mirroring the same project / folder structure of API back-end development (e.g.
Commonproject with appropriate folders).
#
Security
- Deny by default (principle of least privilege).
- When you create an endpoint, always add security immediately.
- Avoid security through obscurity. Assume endpoints will be discovered and tested by bad actors.
#
Testing
- Unit tests must cover core business logic.
- Write unit tests for all input validation rules.
- Write integration tests for all non-trivial endpoints.
- Use mocks/stubs for external services.
#
Input Validation
All incoming data must be validated at the API boundary. Never trust user input, even from internal applications or systems. Proper input validation is critical for application integrity, security, and stability.
- All input models should have validation rules defined in an associated namespace under
.BusinessServices/Validations/{ModelName}/ - Fail fast and loudly: Reject bad input early with clear, actionable error messages. Don’t silently fix or ignore bad data.
- .NET provides built-in validation attributes that can be applied directly to request models. These are automatically enforced by ASP.NET Core Model Binding.
#
Midland is the Client
API development should not be focused on a single technology. It should be designed to satisfy the needs of the company.
To satisfy the company's needs, we have to understand that the consumers will be varied. It could be other APIs, Excel spreadsheets, React clients, third party vendors, Decisions, etc.
While initial development is, in most cases, going to be for a React front end, only considering that React front end will limit the APIs initial usefulness to the rest of the company.
Considerations should be taken to expose enterprise objects that are more complex. But those enterprise objects should also provide the ability to produce the POST, PUT, and DELETE type models allowing interaction with the conventional CRUD methods talked about above.
#
Developer Approach
#
Focus on the Business Requirements
The primary purpose of our code is to enable the business to operate more effectively. While non-functional requirements such as authentication, authorization, and transaction management remain important, they should be handled through shared infrastructure, enabling developers to focus on the necessary business logic.
#
Reduce Cognitive Load
Cognitive load is the demand placed on working memory while a problem is solved, or as learning is taking place.
If the developer has to worry about if a field should be set because the value might be invalid, that is cognitive load.
If the developer has to worry if the user has the rights to update a resource, that is a cognitive load.
If the developer must ensure the value passed in doesn’t violate the data sources limitations, that is another cognitive load.
If a validation rule is no longer needed, removing that rule should have no side effects anyplace in the system. If they have to worry about side effects, that is cognitive load.
As a developer works, they should be focused on the needs of the business for the part of the system they are working on. The fewer considerations at any point increases performance and quality because the developer can focus on a smaller thing.
#
Make the Common Easy
This is just to say many things in programming, especially in enterprise systems, are the same across applications. Settings management across APIs are only different in the values being managed. The process is usually identical across APIs.
Over time, the data source may change. The rules about who or what or how they may change them will vary, but the basic process remains simple CRUD type operations.
Knowing this, the process should be easy to duplicate across APIs.
This is accomplished by various tools.
Code generation can create the models that can accomplish the basic goals but can be easily modified to adapt to specific APIs.
#
Convention Over Configuration
Having to decide in every API where temporary files are saved, how emails will be sent or how the connection to the database will be established forces the developer to consider things that are not relevant to the business units needs. All these things should be decided by our conventions.
Move the load to the Core => Framework
Much of the above can be accomplished by having the Core components shoulder the load. The Core components should rely heavily on the framework it is based on. As long as abstractions and SOLID principles are applied, keeping up with future updates will not be an issue.
An example is filtering, sorting and paging resources. Again, in most cases, what is required can be reduced to a simple data structure and predictable processes. Instead of requiring the developer to think about what is needed in every instance, they can rely upon a known data structure and meta data to perform the work.
#
Allow Adaptations
If the existing paradigms do not allow the developer to achieve the business objective it should be easy for them to vary from them when needed.
One of the ways this is accomplished is the services setup are classes included in the template.
For example the IDataOperations interface is implemented as a class included in the template. If the default implementation does not satisfy the need, it can be changed so that it does. This is also true of other classes such as the Validation Service and Composite Authentication Service.
The Core functionality should not make it difficult to adapt to unique situations. And adapting to those unique situations should not require abandoning the rest of the Core functionality.
#
Isolate Complexity
Our API projects are split into different projects in order to isolate the complexities.
API project is the interface to the outside world.
Business Service contains all the business logic.
Data is only concerned with getting data and putting it.
Common is data structures, interfaces and models exposed to the outside world.
#
SOLID is Archaic but Relevant
SOLID principles remain a foundational approach for maintainable backend systems. While modern architectural patterns may introduce new terminology and variations, SOLID remains a valuable resource for understanding good software design practices and serves as a useful baseline.