Our team is going through a tough and fun period at the moment. The team is a scrum team at ee-mobility that works together with netlight consultants. We migrate from a CloudFoundry environment, where a dedicated team managed for us all the infrastructure components and we were able to focus for the last 2 years on just putting out feature after feature. Now our childhood is over and we tap into the period of adolescence. So now we are migrating all of our services to AWS to run them all ourselves.
But let’s backup a bit and allow me to give you some context what we are building and why we are building it the way we did.
Why are we writing this blog post?
During the preparation of the move we talked to people, read blog post after blog post, asked questions on stack overflow and github. What surprised us the most was the vast variety of different solutions. There wasn’t a “This is how you do it. Full stop.” We haven’t gotten anything super special that makes our solution difficult to migrate. We found a world of EKS, Fargate, Beanstalk, EC2, ECS, Docker Swarm, Jenkins, Bitbucket pipelines, Kops and many more. The team was overwhelmed. So this post is for YOU, mighty reader that looks for some inspiration on what, why and how we picked our stack.
What is our technology stack?
We have roughly 15 microservices that together forms a distributed application. Every application is a Spring Boot 1.5 Java 8 application. We use gradle and jenkins.
What is the domain?
Our platform allows our customers to charge their electric cars on charging stations. It is like a subscription service where we provide access to these stations for a monthly fee.
A big bunch of stations are directly connected to our backend via Rest and Websocket and constantly sends data and commands. In addition there is a React App to support back office tasks like Customer Service, Operations of stations and contract management.
Our customers interact with the solution through a Swift iOS or Kotlin Android app. We have more or less three features that we focus on:
- you charge your car at home,
- at a station at your work, which your employer provides and
- on public charging stations.
Domain Driven Design
From an architecture perspective we made heavy use of Domain Driven Design as a toolset for our application and service design. It helps both for designing the inners of an application (think class design, domain, root aggregates, transaction boundaries and design, repository, service patterns) as well as the entire distributed application (think ubiquitous language, bounded context) so called strategic design.
One service per bounded context
A gut feeling in the early days made us decide to favour many small services rather than few bigger ones. 2 years later we have roughly 15 services, where each service is also a separate deployment unit. Each service encapsulates one bounded context.
Our domain seemed very diverse in the beginning and talking to our domain experts made us belief we had to deliver functionality in many different areas very early, e.g. from directly connecting to charging stations, over contract management to payment and reporting; hence we favoured smaller services that are faster redeployed after a change, easy to understand and the different domains have no opportunity to tangle too much together. Good fences make good neighbours. Different git repositories are really good fences! Requiring this extra HTTP call, makes a developer think twice if this domain entity is part of this bounded context or the other.
A Bounded Context is a boundary for the meaning of a specific domain. In our industry the Charging Station is a very fundamental and central piece that can be found to some extend in many Domain Models in our application, but each Domain defines the meaning of a Charging Station for itself.
We introduced Domain Sketching Workshops right from the beginning. Today you would probably call them Event Storming workshops. The entire (!) team comes together, draws on a whiteboard what the domain entities are and to which bounded context they belong. We are also never afraid of changing a bounded context and moving entities from one domain to another.
Our system has among others, the following Bounded Contexts
Example from the real world
Once the system has reached the point of multiple Bounded Contexts, one challenge arrives, which is not talked about so much and patterns and practices haven’t been shared enough in my perspective: repeating entities in different domains or one concept from the real world has implication on multiple domains.
In a monolith system the core entities tend to grow into enormous classes and ecosystems around them. Change is difficult, feature complexity is high, but creating it in the first place is relative simple, because everything that belongs to the customer domain gets put next to the customer.
A concrete example we struggled with, is the concept of a charging process. The charging process as a concept exists for the driver that sees his current charging process in the app, the Chargepoint Operator uses it to manage the interactions with the physical station and in terms of account and billing it represents the amount and time of a specific charge process. So two domains (Chargepoint Operator and Customer) are involved and know of this concept; both with a different angle on the same underlying concept. There are many ways to handle this, but first it is important to understand that both Bounded Context need to implement their interpretation of the concept. For a young team this may feel repetitive and is hostile to the solution. In this case we even named the concept ‘Session’ in both Domains. Which I, in hindsight, would advise against. Try to find a name that exactly describe what it does or what it is for; avoid general vacuous terms. It leads to conversations like “Do you mean the session in the Customer or in the Chargepoint?”
In the domain of electric charging there isn’t a universal ubiquitous language yet; it is not mature enough. It starts at a simple entity: the thing you use to charge your car; this wall box, charging unit, charge pole, charging station, charging spot, controller, you name it. Every application and backend we integrated brought their own language. Domain experts use their favourite word too. We set out to defend our very own language! But first we had to define it, so we started with Domain and Bounded Context sessions on the whiteboard until we agreed on a language. For us these are charging stations that have multiple spots just to give you one example.
Example from the real world
With that said how do you defend this language inside your software? How do you make sure that another language from another application doesn’t creep into your system and pollutes it when you consume it. Again good fences make good neighbours we added a service in between that does nothing else, but translating from the language of the consumed api/application and our platform. We called them adapters and they are separate services in our landscape that each live in their own git repository and get deployed as independent deployment units. This may sound over engineered and unnecessary, but so far we are happy with the solution. We successfully defended our language!A Bounded Context is a boundary for the meaning of a specific domain. In our industry the Charging Station is a very fundamental and central piece that can be found to some extend in many Domain Models in our application, but each Domain defines the meaning of a Charging Station for itself.
The Chargepoint domain uses the term Charging Station, which can have multiple spots and each spot has a different plug.
An external service uses the word Chargepoint for a single spot, which has specific ConnectorTypes and doesn’t know about the physical concept of a charging station. The adapter serves as a Anti-corruption layer to our Chargepoint domain.
Domain entities, working with an ORMapper and what objects do you expose in your RestController?
By now you probably get the feeling that our team is a little obsessed with design principles and that we like to stick to them. Here is another one for your reading pleasure. We have at least 3 different object types; the objects that are retrieved from the database via hibernate are DataAccessObjects, the Domain is obviously built around Domain Entities, Events and Services and the data structure exposed via RestControllers are DataTransferObjects. Mapping happens via ModelMapper and the translation is most of the time straight forward without much logic. We do this, because we didn’t want to have huge classes that are full of ORMapper and JSON Serialisation annotation in one file. It helps to keep the API stable. A change in the storing structure for example should not have any impact on the API, even though it means repetitive mapping at run time and more code written during development, but complexity in this activity is low.
After this introduction into our domain, design activities and design choices, in another post we’ll share more of our journey towards AWS ECS. Stay tuned.
We are interested in your domain driven design challenges and eureka moments. Please share them with us in the comments below.
This blog post is written by Steve Behrendt and part of the collaboration of Netlight Munich and eeMobility in the emerging field of electric charging. Together, we build a system to enable corporate fleets to use renewable energy and pioneer in the field of smart charging.