Glossary¶
We are trying to make the purpose and best use of svcs as clear as possible, even if you’re unfamiliar with all the concepts it’s based on. As such, the glossary got slightly out of hand, but we hope it’s to your advantage.
If you want a more narrative introduction into the concepts, we strongly recommend reading the wonderful book Architecture Patterns with Python (you can read it for free on the web).
- Service¶
Unfortunately, service is a highly overloaded term in software engineering, but we’ve resisted to come up with a new term that’s even more confusing.
In our context, it’s a local object managed by svcs. This can be anything you’d like to be loosely coupled to your application code. For example, a database connection, a web API client, or a cache.
They’re usually also something you need to configure before using and can’t just instantiate directly in your business code.
Important
One key aspect of these services is that they provide behavior, but have no state that is relevant to the business logic. They are pure doers. They query databases, make HTTP requests, store data in caches, or delete files.
But they don’t do anything business-relevant neither with the data they send out, nor with the data they get back.
In an ideal world, they only pass data hence and forth with the domain model which decides what happens next based on plain objects.
- Resource¶
Used interchangeably with service, but also a heavily overloaded term.
- Dependency¶
Used interchangeably with service, but also a heavily overloaded term.
- Service Layer¶
The service layer – sometimes called the orchestration layer or the use-case layer – is where your business logic (also known as the domain model) meets your services.
Since services can use other services, it’s not a flat layer but more of a tree. The entry point is called from your composition root (for example, your web framework’s views) and coordinates database transactions, other services and, the domain model.
If you pass in all the services it needs, it’s dependency injection. If you look up the services within the service layer, it’s service location.
An example would be a function for adding users to organizations that takes a database Unit of Work, an email notification queue, an organization ID and a user ID as parameters and queries the domain model if it’s OK:
def add_user_to_org(unit_of_work, mail_q, org_id, user_id): """ Service Layer entry point. """ with unit_of_work: org = unit_of_work.organizations.get(org_id) user = unit_of_work.users.get(user_id) domain_model.check_if_can_add_user_to_org(org, user) unit_of_work.organizations.add_user(org_id, user_id) mail_q.send_welcome_mail(user.email, user.name, org.name) unit_of_work.commit()
In this case, the
unit_of_work
andmail_q
parameters are services that are used by the serviceadd_user_to_org()
and are passed – or: injected – by the composition root.The business rules are enforced by
domain_model.check_if_can_add_user_to_org()
which is a pure function working on plain domain objects and doesn’t use any services.
As you can see, ideally you call service layer functions from your views and pass them all the services it needs to do their job. That keeps the service layer clean from any framework-specific code and makes it easy to test. You could also call that function from a CLI entry point, a work queue, or a test.
Admittedly, in simple cases it’s possible that the whole logic is in the service layer and you don’t need a separate domain model. Don’t let that discourage you.
See also
The fourth chapter of the wonderful Architecture Patterns with Python book called Flask API and Service Layer (you can read it for free on the web).
- Service Location¶
See Service Locator.
- Service Locator¶
The architectural pattern implemented by svcs and a way to achieve Inversion of Control and loose coupling.
Like dependency injection, it depends on having a central registry of factories for services that aren’t instantiated directly in your business code.
Unlike dependency injection, it’s imperative: You ask for services explicitly at runtime instead of having them injected into your business code. The injection also usually requires opaque magic and meddling with your function/method definitions when using dependency injection frameworks.
The active acquisition of services by calling
get()
when you know for sure you’re going to need them avoids the conundrum of either having to pass a factory (like a connection pool – which also puts the onus of cleanup on you) or eagerly creating services that you never use:def view(request): if request.form.valid(): # Form is valid; only NOW get a DB connection # and pass it into your service layer. return handle_form_data( svcs_from(request).get(Database), form.data, ) raise InvalidFormError()
Important
If you use svcs like in the example above, you’re doing dependency injection – and that’s a Good Thing™.
Obtaining the database using
svcs.Container.get()
is service location, but passing it into your service layer – without using it yourself – makes the view a composition root andhandle_form_data()
the entry point into your service layer.You could say that you’re moving your composition root into the view where it acquires services on demand as it needs them.
We strongly recommend using svcs like this.
Classic service location in the service layer would look something like this:
from flask import request def view(): if request.form.valid(): return handle_form_data(form.data) def handle_form_data(data): svcs.flask.get(Database).do_database_stuff(data)
We do not recommend using svcs like this because it’s harder to test and reason about.
- Dependency Injection¶
Dependency Injection means that the service layer is called with all services it needs to do its job. It is a fundamental technique to achieve loose coupling between your business code and the services it depends on – and to achieve Inversion of Control.
For example, in the following code, we have a function that adds a user to a database then sends them an email. To do that, it needs an
SmtpSender
and aDbConnection
which it constructs. This makes the code hard to test, since we don’t want our unit tests to send people emails.def add_user(email): smtp = SmtpSender() db = get_database_connection() try: user = db.create_user(email) smtp.send_welcome_email(user) except DuplicateUserError: log.warning("Duplicate user", email=email)
To fix that, we need to take control of the dependencies out of the function, and pass them in as parameters – we inject them:
def add_user(email, smtp, db): try: user = db.create_user(email) smtp.send_welcome_email(user) except DuplicateUserError: log.warning("Duplicate user", email=email)
Often when talking about dependency injection, people think of dependency injection frameworks that use decorators or other magic to inject services into their code. But dependency injection just means that services are passed from the outside, with no control over how that happens (hence Inversion of Control).
Our preferred way to use svcs is to use it as a service locator, but only in your composition root to acquire necessary services. Which then injects said services into your service layer:
def view(request): """ View and composition root. """ return do_something(svcs_from(request).get(Database)) def do_something(db): """ Service layer. """ db.do_database_stuff()
In this case
view
uses svcs to locate aDatabase
and then injects it intodo_something()
.do_something()
doesn’t know wheredb
is coming from and how it was created. It only knows it needs to do database stuff.See also
incant, a lovely dependency injection framework for Python.
- Composition Root¶
The place that acquires all the services your application needs and calls into the service layer.
Common types are web framework views, CLI command entry points, or test fixtures.
We recommend using svcs in your composition roots to get all the services you need and then pass them into your service layer.
- IoC¶
See Inversion of Control.
- Inversion of Control¶
Inversion of Control (IoC) describes the concept of your code being invoked by someone else – usually based on some kind of configuration.
That’s why it’s sometimes called the Hollywood Principle: “Don’t call us, we’ll call you.”
From the perspective of the user, this usually means that third-party lower-level code (socket listeners, web frameworks, object observers) invokes your higher-level code (business logic).
A service locator like svcs is an example for IoC, because you tell it how to create services by defining a factory for a type and svcs will then invoke that factory when the user asks for a service of said type.
Note
Weirdly, of all the glossary entries, this one is the most controversial. We’ve tried to keep it as general as possible while ensuring it remains useful, but it is impossible to cover everyone’s pet understanding of the concept. It has been rewritten several times, and we think this will have to do now.
Please refer to the linked sources to get an idea of the various facets of the concept.
See also
InversionOfControl by Martin Fowler.
Hoist Your I/O – a 2015 talk by Brandon Rhodes.
- Late Binding¶
Late binding is a very general term, but in our context, we mean that the concrete instance type of a service is only determined (bound) when it’s requested using
svcs.Container.get()
.This makes your code very testable because you can easily replace services with test objects without having to use brittle and intrusive methods like monkey-patching.
On the other hand, the downside is that the registry might not have been configured to provide the requested service, which will cause a
svcs.exceptions.ServiceNotFoundError
at runtime.- Dependency Inversion Principle¶
Sometimes confused with Dependency Injection due to the similarity of “Injection” and “Inversion”, but only tangentially related. It’s the D in SOLID and also known as “program against interfaces, not implementations”.
The goal is to achieve loose coupling by making code depend on interfaces instead of concrete implementations.
Since Python is dynamic and you’re free to ignore type hints in your tests, the usefulness isn’t quite as high as in more static languages. Nobody will stop you from passing a Mock where a Django
QuerySet
is expected.See also
Typing Caveats regarding abstract classes and svcs.
https://en.wikipedia.org/wiki/Dependency_inversion_principle
The third chapter of the wonderful Architecture Patterns with Python book called On Coupling and Abstractions (you can read it for free on the web).
- Hexagonal Architecture¶
Also known as “ports and adapters”, “onion architecture”, or “clean architecture”. They all have slightly different meaning, but the core idea is the same.
It divides a system into several loosely-coupled interchangeable components, such as the database, the user interface, test scripts, and interfaces with other systems (also known as services) with the application core (also known as business code) in the middle.
The business code doesn’t use those services directly, but only through interfaces that are called ports here.
Now a service locator like svcs can be used to register factories for those interfaces such that the business code can use the services as adapters to the real world (for example, query a database) without knowing what they are and where they come from.
It is therefore a special case of the Dependency Inversion Principle.
The svcs logo is a hexagon to remind you of this architecture.
See also
https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)
Functional Core, Imperative Shell – a screencast by Gary Bernhardt.
The Clean Architecture in Python – a 2014 talk by Brandon Rhodes.
- Service Discovery¶
An completely unrelated concept for finding remote services (for example, web services, database servers, and so on).
Another sad consequence of the overloaded term service.
Common examples of implementations are Consul, etcd, or Apache ZooKeeper.