We can show its relevance in a more familiar example.
The mental process mentioned above is one reason why Test Driven Development has such a strong impact on code quality and reliability.
If you are interested in good practices, you have probably at least tried using TDD for a while and, even if you finally came to the conclusion that the benefits are not worth the hassle, you most likely discovered something peculiar about your code.
Using software as a client before implementing the solution forces us to first consider use cases and user convenience; we work only on code needed to fulfill assertions made in tests, limiting the resulting complexity to an absolute minimum.
It also makes the use of mockups and fakes very desirable, allowing for usage of the interface only during development and delegating external parts for later.
By moving test writing one step ahead of the production code, we end up with more modular and maintainable design; it gives us an opportunity to come up with usable interface long before getting our hands dirty with technical details.
This is a great practical exercise to better understand the importance of using contracts when reasoning about software that communicates.
All we need to do now is to apply this principle to a higher layer of abstraction: APIs.
Make collaboration easier with contracts“Programming is a social activity.
”― Robert C.
MartinSo coming back to our everyday reality; treating a schema as a product of business code means that every change in the backend can cause interface changes.
These are fine for backend development, since everything builds up from core business entities, but they introduce code dependency between the client and server side, making it implicitly the client’s responsibility to align with changes.
Our job as developers is to lay the foundation from which it is easy to do good and hard to make costly mistakes.
The problems we discuss here are not related to the attitude or communication of developers; this kind of code dependency results in all sorts of organization artifacts, component divisions, communication patterns, CI/CD flows and planning blind-spots that lead to avoidable conflicts, bugs, and poor design.
Dependency: the problem deep downThe problems we see with GraphQL are not new or unique; they strongly resemble those encountered by designers of Object Oriented software or micro-services architects.
History is full of lessons learned the hard way.
This is the reason for the Dependency Inversion Principle (DIP), Inversion of Control (IoC) and Dependency Injection (DI).
In our particular scenario, DIP applies the best.
Dependency InversionWhat is DIP?1.
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
Abstractions should not depend on details.
Details should depend on abstractions.
The underlying idea behind this principle is to reverse runtime and code dependency in a project, giving us the freedom to use any implementation we want, as long as it is based on the right prototype.
We basically introduce contract-based polymorphism into our system.
Both the client and the implementation depend on a common interface, thus breaking the dependency nightmare and making it simple to check for compatibility issues.
In the OOP world, this results in Interfaces or Abstract Classes that are a base for concrete implementations; production algorithms are being built by injecting anticipated instances into places already defined by their interface in the general flow (Dependency Injection).
For example, we may use a fake building block when testing or switch strategy on the fly, depending on the situation.
But I am not writing service with classes in 2019!Rules are there for a good reason and extend far outside the OOP kingdom.
Such a guiding principle (DIP) is useful wherever we encounter dependency issues and a need for polymorphic behavior; you should keep it in mind even if, like me, you are skeptical about object-oriented programming.
Personally, I find SOLID rules much more suited for the component-scale design than for business logic modules.
How to apply it then to our GraphQL service architecture?TDD might help introduce DIP into the process of writing backend units since the programmer is also later the code user, but that trick won’t work on an API as the code is usually passed over to a frontend team.
The most obvious solution is to write SDL first, then give it to both the frontend and backend side to independently implement.
There are a few existing solutions to mock GraphQL SDL with fake data and make it possible to test your client without any existing backend.
Another problem lies in the timing.
A new API is nonexistent until the backend work is mostly finished and the interface is generated, meaning that client-side work happens once the backend is finished.
Good implementations should minimize effortWe should also think about how failure to keep layer content on similar abstraction levels causes a great number of problems.
Very low-level details are too easily introduced into schema and data formatting when using code-first solutions, which would never occur to us as a valid option if we tried to come up with the schema shape first.
It’s clear that APIs must be driven by their client use cases, ease of use, and the need to allow simultaneous frontend and backend implementation efforts.
All of this comes almost for free when using schema-first GraphQL development for its contract-based approach.
The other benefits of a Schema-first approachExpressing GraphQL in its native language is ideal but, in the complex world of development, you might find keeping common knowledge more valuable.
Hiding your database schema makes it less likely to be directly exposed in your client’s interface in a well-known anti-pattern.
This can lead to numerous problems like naming issues and schema updates.
Making schema an entity on its own could allow for a more flexible approach to designing your architecture and leave more wiggle room for moving stuff around late in the development cycle.
Last but not least, it is a much better approach to integrate into test-first development and QA processes.
Make schema changes, along with black box acceptance testing, an integral part of your definition of done and use them to catch mistakes early into development.
The case for Code-first developmentSchema-first development comes with a certain price tag.
It is definitely more verbose on the backend side, almost certainly resulting in some level of code duplication and an additional layer of abstraction to manage manually that will be responsible for field and resolver mapping.
This is true for all patterns dealing with Dependency Inversion Principle and apparently does not render them useless; either way, in some edge-case circumstances this may result in a lot of additional code and the end may not be worth the means.
We also need to be aware of quiet regression.
Imagine enumeration type as expanded in SDL without the backend team noticing, you might need to check for additional value in your core logic — but the type system will probably catch only disappearing elements.
This issue is platform-specific and can be prevented by proper tooling.
Many projects today consist of multiple services and schema relegation, or stitching for such architecture might also be more cumbersome and involve more upfront investment.
Since not all platforms will come up with convenient code-first solutions and tooling, it is also hard to predict if there will be any compatibility problems.
You should also consider a code-first approach if you need to keep the shape of the API close to the data model, when you need to constantly align trivial changes between API and implementation, and when the frontend is not a priority.
You can read more about that:The Problems of "Schema-First" GraphQL Server Development | PrismaThis article gives an overview of the current state of the GraphQL server development space.
Here's a quick outline of…www.
ioAriadne: the beauty in simplicityComing from a Django background, we naturally ended up using the Graphene ecosystem for our GraphQL APIs, which is code-first and is a heavily abstract framework to work with.
As we encountered problems with this solution, some of which were related to the code-first approach, we realized it wasn’t for us.
Unable to find a suitable schema-first solution for Python development, we eventually decided to just write our own!.Check out Ariadne, which we think makes pythonic GraphQL server development simple and fun.
It is deeply rooted in our appreciation for simplicity and extensibility, giving you all the tools needed to mindfully craft your reasonable service.
mirumee/ariadneAriadne is a Python library for implementing GraphQL servers using schema-first approach.
comThe last wordAll things considered, schema-first development should be considered a default choice if no additional constraints are obvious, but the whole GraphQL ecosystem is in the early stage of rapid growth and it is hard to predict what the future will bring.
This first article is a manifesto but we’ve got a lot of other interesting ideas around the Schema-first approach that we will be sharing in future articles.
So, keep your eyes open for those.
We love to hear your thoughts on our thoughts, so please leave a comment.
Mirumee guides clients through their digital transformation by providing a wide range of services from design and architecture, through business process automation, to machine learning.
We tailor services to the needs of organizations as diverse as governments and disruptive innovators on the ‘Forbes 30 Under 30’ list.
Find out more by visiting our services page.