This document provides an overview of architecting microservices using .NET. It discusses why microservices are used, common architecture patterns, and implementation considerations. Key points include:
- Independent, loosely coupled services that are fault tolerant and easy to scale are goals of a microservices architecture.
- Communication between services should be kept simple, using either synchronous HTTP or asynchronous messaging. Synchronous calls can lead to temporal coupling so circuit breakers and failure handling are important.
- Domain-driven design principles like bounded contexts and separating queries from commands (CQRS) can help define appropriate service boundaries and responsibilities.
- Event sourcing avoids shared state and two-phase commits by persisting a sequence of events rather than
3. Preamble
īAs an industry weâre still learning and maturing
īTooling for microservices is akin to frameworks
for JavaScript
īMany microservice implementations arenât
âpureââĻ and thatâs OK
īDocker onWindows is still a work in progress
īOut of scope: Azure Service Fabric,Akka.NET,
etc.
īLots of bullet points so you can use this deck as
a reference ī
6. OK. More
seriouslyâĻ
īGreater flexibility & scalability
īMore readily and easily evolvable systems
īIndependently deployable parts
īImproved technical agility
īIndependent development teams
7. A few more
reasons?
Sure!
īResilience.A failure in one service shouldnât
wipe out the whole system.
īTech stack flexibility. Right tool for the right
job.
īSmaller services are easier to understand and
maintain.
īA potential migration approach for legacy
systems
9. WTF!OMG!
Gah! <update
resume />
īIsnât this meant to be easy?!
īI canât tell how it fits together anymore!
īItâs more brittle now than it ever was!
īPerformance is terrible!!
īI still need to make code changes in multiple
services at once!Yyyy?!
10. Why?
īDistributed systems are hard
īEventual consistency messes with your head
īOld habits result in a distributed âbig ball of
mudâ
īChallenges debugging across multiple services
īPeople tend to forget good design practices
through underuse (legacy habits)
11. Other things
to deal with
īShared databases across services?
īDo transactions across services mean we need 2
phase commits?
īWhat is the current âversionâ of the system?
īHow do we do authentication and authorization
at a service level?
īWhat do testers actually, you knowâĻ test?
13. A quick
reminder
Architecture isnât about technology alone.
Think of your teams and their skills.
Consider your organisationâs structure.
Keep your architecture simple so you can meet
your customerâs needs in the most cost effective
way.
Donât build what you donât need.
Donât build what you might need!
18. KeepComms
Simple
Use language & platform agnostic
communications
īOne synchronous approach
(e.g. JSON over HTTP)
īOne asynchronous approach
(e.g. AMQP using RabbitMq)
Consistency in comms reduces complexity.
19. Synchronous
Comms infers
Temporal
Coupling
īIf your services use synchronous comms, you
need to handle failures and timeouts.
īUse a circuit breaker pattern
īDesign with failures in mind (and test it!)
īNetflix created the âchaos monkeyâ for testing
failures in production.
http://www.lybecker.com/blog/2013/08/07/automatic-retry-and-circuit-breaker-made-easy/
20. Identify your
business
transactions
īOne user request may result in tens, or
hundreds, of microservice calls
īTreat each user request as a logical business
transaction
īAdd a correlation ID to every user/UI request
īAids with request tracing and performance
optimisation
īAids with debugging, failure diagnosis and
recovery
21. Evolvable
APIs via
Consumer
Driven
Contracts
A concept from the SOA days:
âServices aren't really loosely coupled if all parties
to a piece of service functionality must change at
the same time.â
In SOA daysWSDLs and XSDs were meant to
solve this.YeahâĻ right.
With HTTP APIs, have a look at Pact
http://www.infoq.com/articles/consumer-driven-contracts
https://github.com/SEEK-Jobs/pact-net
https://www.youtube.com/watch?v=SMadH_ALLII
22. Loose
Coupling and
Service
Discovery
īLoose coupling implies no hard coded URLs.
īService discovery isnât new (remember UDDI?)
īMicroservices need a discovery mechanism.
ī E.g. Consul.io & Microphone
ī https://github.com/rogeralsing/Microphone
25. Design
Patterns &
Components
īDomain Driven Design â Align services to
Domain Contexts, Aggregates & Services
īCQRS â Command Query Responsibility
Separation. Scale reads and writes
independently.
īSQL/NoSQL â Persistent, easily rebuilt caches
for query services.
īVersioning â APIs are your contracts, not
versions of binaries.
26. Design
Patterns &
Components
Message Bus â Reliable, async comms.
Optimistic Concurrency â No locking!
Event Sourcing â Persist events, not state.Avoid
2-PC hassles.
Application Services â encapsulate access to
microservices; optimise for client needs.
27. Event
Sourcing.
Really?
īYes, really :-)
īWhen a domain object is updated, we need to
communicate that change to all the other
interested microservices.
īWe could use 2-phase commit, and we could
also drink battery acid.
īWith ES we simply save a domain event to our
event store and then publish it on the bus.
īInterested other services subscribe to events
and take action as they see fit.
28. WARNINGS
īThe â100 lineâ rule is just silly.
īThese nano-services are effectively a service-
per-method.
īTurn your app into thousands of RPC calls!Yay!
īServices should be âbusiness servicesâ and
provide business value.Again, apply DDD
concepts to determine service boundaries.
29. Rule of
thumb for
sizing
microservices
īHave a single purpose
ī E.g. manage state of domain entities
ī E.g. send emails
ī E.g. authenticate users
īBe unaware of other services (in the core)
īConsider use case boundaries/bounded
contexts
32. Web API Controller
Request (HTTP)
Aggregate
Event Handler(s)
Event Store
Domain Micro Service
Command
Message Bus (publish)
Command Handler
Command(s)
Event Store Repository
Save New Events
Event(s) Event(s)
33. Web API Controller
Query (HTTP)
Query Handler
Event Handler(s)
Message Bus (subscribe)
Query Micro Service
Event(s)
Read Model
Persistence
(akaView Store)
Consider splitting
here when scaling
beyond a single
instance to avoid
competing
consumers
Query
Updates
38. Boundaries?
User Story?
As the coffee shop owner
I want to define the products
that are offered for sale
So I can set my menu
Use Case?
Manage Products
ī View products
ī Create/Update products
40. SceneSetting
īDomain entities are in the application core and
updated via methods
īCommands/Queries are the adapters and ports
for our services
īCQRS â use separate microservices for
commands and queries
42. Event
Sourcing
impacts
design
īAs Event Sourcing is used, domain objects
ONLY update their state by processing an
event.
īCommands do not update state.
īCommands cause events to fire.
īUseful when replaying events.
43. public class Product : Aggregate
{
private Product() { }
public Product(Guid id, string name, string description, decimal price)
{
ValidateName(name);
ApplyEvent(new ProductCreated(id, name, description, price));
}
public string Name { get; private set; }
public string Description { get; private set; }
public decimal Price { get; set; }
private void Apply(ProductCreated e)
{
Id = e.Id;
Name = e.Name;
Description = e.Description;
Price = e.Price;
}
44. Aggregate
BaseClass
īHolds unsaved events
īHelper method to reapply events when
rehydrating an object from an event stream
īProvides a helper method to apply an event of
any type and increment the entityâs version
property
45. public abstract class Aggregate
{
public void LoadStateFromHistory(IEnumerable<Event> history)
{
foreach (var e in history) ApplyEvent(e, false);
}
protected internal void ApplyEvent(Event @event)
{
ApplyEvent(@event, true);
}
protected virtual void ApplyEvent(Event @event, bool isNew)
{
this.AsDynamic().Apply(@event);
if (isNew)
{
@event.Version = ++Version;
events.Add(@event);
}
else
{
Version = @event.Version;
}
}
46. public class Product : Aggregate
{
private void Apply(ProductNameChanged e)
{
Name = e.NewName;
}
public void ChangeName(string newName, int originalVersion)
{
ValidateName(newName);
ValidateVersion(originalVersion);
ApplyEvent(new ProductNameChanged(Id, newName));
}
47. Port:
Command
Handlers
īCommands do not have to map 1:1 to our
internal methods.
īCommands (the ports) represent the inbound
contract our consumers rely on.
īInternal implementation and any domain
events are up to us.
īCommand objects are just property bags.
48. public class ProductCommandHandlers
{
private readonly IRepository repository;
public ProductCommandHandlers(IRepository repository)
{
this.repository = repository;
}
public void Handle(CreateProduct message)
{
var product = new Products.Domain.Product(message.Id, message.Name,
message.Description, message.Price);
repository.Save(product);
}
50. [HttpPost]
public IHttpActionResult Post(CreateProductCommand cmd)
{
if (string.IsNullOrWhiteSpace(cmd.Name))
{
var response = new HttpResponseMessage(HttpStatusCode.Forbidden) { //âĻ }
throw new HttpResponseException(response);
}
try
{
var command = new CreateProduct(Guid.NewGuid(), cmd.Name, cmd.Description, cmd.Price);
handler.Handle(command);
var link = new Uri(string.Format("http://localhost:8181/api/products/{0}", command.Id));
return Created<CreateProduct>(link, command);
}
catch (AggregateNotFoundException) { return NotFound(); }
catch (AggregateDeletedException) { return Conflict(); }
}
51. Adapters:
Database &
Message Bus
īRepository pattern to encapsulate data access
īEvent sourcing; persist events not state.
īImmediately publish an event on the bus
ī Note:This approach may fail to publish an event
ī Can be prevented by using Event Store as a pub/sub
mechanism
ī Can also be prevented by publishing to the bus and using a
separate microservice to subscribe to and persist events to
the EventStore
ī Personal choice: RabbitMq for ease of use & HA/clustering
52. public async Task SaveAsync<TAggregate>(TAggregate aggregate) where TAggregate : Aggregate
{
//...
var streamName = AggregateIdToStreamName(aggregate.GetType(), aggregate.Id);
var eventsToPublish = aggregate.GetUncommittedEvents();
//...
if (eventsToSave.Count < WritePageSize)
{
await eventStoreConnection.AppendToStreamAsync(streamName, expectedVersion, eventsToSave);
}
else { //... multiple writes to event store, in a transaction }
if (bus != null)
{
foreach (var e in eventsToPublish) { bus.Publish(e); }
}
aggregate.MarkEventsAsCommitted();
}
56. var eventMappings = new EventHandlerDiscovery().Scan(productView).Handlers;
var subscriptionName = "admin_readmodel";
var topicFilter1 = "Admin.Common.Events";
var b = RabbitHutch.CreateBus("host=localhost");
b.Subscribe<PublishedMessage>(subscriptionName, m =>
{
Aggregate handler;
var messageType = Type.GetType(m.MessageTypeName);
var handlerFound = eventMappings.TryGetValue(messageType, out handler);
if (handlerFound)
{
var @event = JsonConvert.DeserializeObject(m.SerialisedMessage, messageType);
handler.AsDynamic().ApplyEvent(@event, ((Event)@event).Version);
}
},
q => q.WithTopic(topicFilter1));
60. Redis
Repository
īRedis: A key/value store, with fries
īCollections stored as âsetsâ
īConvention approach to ease implementation
ī Single objects stored using FQ type name
ī Key = MyApp.TypeName:ID
ī Value = JSON serialised object
ī All keys stored in a set, named using FQTN
ī Key = MyApp.TypeNameSet
ī Values = MyApp.TypeName:ID1, MyApp.TypeName:ID2, etc
īRedis can dereference keys in a Set, avoiding
N+1 queries.
61. public IEnumerable<T> GetAll()
{
var get = new RedisValue[] { InstanceName() + "*" };
var result = database.SortAsync(SetName(), sortType: SortType.Alphabetic, by: "nosort", get: get).Result;
var readObjects = result.Select(v => JsonConvert.DeserializeObject<T>(v)).AsEnumerable();
return readObjects;
}
public void Insert(T t)
{
var serialised = JsonConvert.SerializeObject(t);
var key = Key(t.Id);
var transaction = database.CreateTransaction();
transaction.StringSetAsync(key, serialised);
transaction.SetAddAsync(SetName(), t.Id.ToString("N"));
var committed = transaction.ExecuteAsync().Result;
if (!committed)
{
throw new ApplicationException("transaction failed. Now what?");
}
}
64. Hey, Mister!
I donât want
your Docker
Kool-Aid!
Thatâs cool.You donât need Docker (or
containers).
Always get the latest code you need.
Then manually build & run all of the services on
your dev box each time you test.
Use scripting to make it a little less painful.
Side-effect: Encourages a low number of
services.
65. Use service
subsets to
ease the pain
īUse test/mock services.
īOnly spin up the services you need to test your
work, and avoid all the other services that exist
īRequires a bit more knowledge around what
services to use and what to mock.
īCould also use tools like wiremock to intercept
and respond to HTTP requests.
66. Production in
a box
īUse Docker images and Docker-Compose to
automatically build and run environments that
match production.
īYou may be limited by the resources of your dev
box (RAM, CPU cores, disk)
īCould also use Azure RM templates or Azure
Container Services to spin up environments in
the cloud. (or the equivalent in AWS)
67. Containers
and
versioning
īDonât think about âupgradingâ microservices.
īContainers are immutable.
īYou donât upgrade them; you replace them.
īNo more binary promotions to prod.
īYou promote containers to prod.
īHave an image repository (e.g. artifactory)
68. What is the
version of the
app?
īConsumer Driven Contracts reduce the care
factor somewhat.
īConsider having an environment configuration
file
ī List the version of each microservice that has been
tested as part of a âknown goodâ configuration
-- OR --
īIgnore versioning and rely on monitoring in
production to report problems, and rollback
changes quickly